mirror of
https://github.com/AppleCommander/AppleCommander.git
synced 2026-10-24 23:02:27 +00:00
Merge pull request #329 from AppleCommander/feature/editable-attributes
Feature/editable attributes
This commit is contained in:
25 files changed
+1101
-286
No files matched your search
@@ -12,4 +12,5 @@ module org.applecommander.javafx {
|
||||
opens org.applecommander.javafx to javafx.graphics;
|
||||
opens org.applecommander.javafx.settings to javafx.graphics;
|
||||
opens org.applecommander.javafx.wizard to javafx.graphics;
|
||||
opens org.applecommander.javafx.filter to javafx.graphics;
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
*/
|
||||
package org.applecommander.javafx;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
@@ -26,13 +28,16 @@ import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.applecommander.applesingle.AppleSingle;
|
||||
import org.applecommander.applesingle.FileDatesInfo;
|
||||
import org.applecommander.applesingle.ProdosFileInfo;
|
||||
@@ -44,12 +49,19 @@ import org.applecommander.bastools.api.model.Program;
|
||||
import org.applecommander.bastools.api.model.Token;
|
||||
import org.applecommander.capability.Capability;
|
||||
import org.applecommander.filestore.*;
|
||||
import org.applecommander.javafx.filter.CharacterRestrictionFilter;
|
||||
import org.applecommander.javafx.filter.CompositeFilter;
|
||||
import org.applecommander.javafx.filter.FixedLengthFilter;
|
||||
import org.applecommander.javafx.settings.ExportOption;
|
||||
import org.applecommander.javafx.settings.Settings;
|
||||
import org.applecommander.source.Source;
|
||||
import org.applecommander.source.Sources;
|
||||
import org.applecommander.transfer.ProdosAttributePreservation;
|
||||
import org.applecommander.transfer.ProdosAttributes;
|
||||
import org.applecommander.ui.ColumnDataType;
|
||||
import org.applecommander.ui.ColumnOption;
|
||||
import org.applecommander.ui.DisplayColumn;
|
||||
import org.applecommander.ui.DisplayMode;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
@@ -59,6 +71,7 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import static org.applecommander.javafx.FxUtils.*;
|
||||
|
||||
@@ -80,13 +93,16 @@ public class FileView extends BorderPane {
|
||||
// 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;
|
||||
// And the detail grid follows the file table!
|
||||
private GridPane fileDetailGrid;
|
||||
|
||||
private final ObservableList<Directory> directoryPath = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Directory> selectedDirectory = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<DisplayColumn.Mode> listingMode = new SimpleObjectProperty<>(DisplayColumn.Mode.NATIVE);
|
||||
private final ObjectProperty<DisplayMode> listingMode = new SimpleObjectProperty<>(DisplayMode.NATIVE);
|
||||
private final SimpleBooleanProperty supportsDirectories = new SimpleBooleanProperty(false);
|
||||
private final SimpleBooleanProperty supportsFileDeletion = new SimpleBooleanProperty(false);
|
||||
private final ObservableList<FileEntry> fileEntries = FXCollections.observableArrayList();
|
||||
private final BooleanProperty showingDetailProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public FileView(FileStoreWindow fileStoreWindow, ToolBar toolBar) {
|
||||
this.fileStoreWindow = fileStoreWindow;
|
||||
@@ -125,7 +141,7 @@ public class FileView extends BorderPane {
|
||||
if (newValue != null) {
|
||||
directoryPath.clear();
|
||||
directoryPath.add(newValue.getRootDirectory());
|
||||
listingMode.setValue(DisplayColumn.Mode.NATIVE);
|
||||
listingMode.setValue(DisplayMode.NATIVE);
|
||||
supportsDirectories.setValue(newValue.can(Capability.SUPPORTS_DIRECTORIES));
|
||||
supportsFileDeletion.setValue(newValue.can(Capability.DELETE_FILES));
|
||||
}
|
||||
@@ -134,8 +150,8 @@ public class FileView extends BorderPane {
|
||||
fileStoreWindow.viewModeProperty().addListener((_, _, _) -> populateDiskRows());
|
||||
|
||||
listingMode.addListener((_, _, newValue) -> {
|
||||
nativeToolButton.setSelected(newValue == DisplayColumn.Mode.NATIVE);
|
||||
detailToolButton.setSelected(newValue == DisplayColumn.Mode.DETAIL);
|
||||
nativeToolButton.setSelected(newValue == DisplayMode.NATIVE);
|
||||
detailToolButton.setSelected(newValue == DisplayMode.DETAIL);
|
||||
fileTable.getColumns().forEach(column -> {
|
||||
if (column.getUserData() instanceof DisplayColumn displayColumn) {
|
||||
column.visibleProperty().setValue(displayColumn.supports(newValue));
|
||||
@@ -197,6 +213,28 @@ public class FileView extends BorderPane {
|
||||
fileTable.setPlaceholder(placeholder);
|
||||
setCenter(fileTable);
|
||||
|
||||
Button detailToggle = FxUtils.createSmallToggleButton("pane-right.png", "pane-left.png", showingDetailProperty);
|
||||
detailToggle.disableProperty().bind(fileTable.getSelectionModel().selectedItemProperty().isNull());
|
||||
fileDetailGrid = new GridPane();
|
||||
ScrollPane scrollPane = new ScrollPane(fileDetailGrid);
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setStyle("-fx-background-color: transparent;");
|
||||
scrollPane.visibleProperty().bind(showingDetailProperty);
|
||||
scrollPane.managedProperty().bind(showingDetailProperty);
|
||||
VBox detailPane = new VBox(detailToggle, scrollPane);
|
||||
detailPane.setAlignment(Pos.TOP_LEFT);
|
||||
detailPane.setSpacing(5);
|
||||
detailPane.setPadding(new Insets(5));
|
||||
setRight(detailPane);
|
||||
fileTable.getSelectionModel().selectedItemProperty().addListener((_, _, newFileEntry) -> {
|
||||
if (newFileEntry != null) {
|
||||
populateFileDetailGrid(newFileEntry);
|
||||
}
|
||||
});
|
||||
fileStoreWindow.fileStoreSelection().selectedItemProperty().addListener((_, _, _) -> {
|
||||
showingDetailProperty.set(false);
|
||||
});
|
||||
|
||||
fileTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
|
||||
fileTable.setItems(FXCollections.emptyObservableList());
|
||||
fileTable.setOnMouseClicked(this::onMouseClickedHandler);
|
||||
@@ -207,6 +245,106 @@ public class FileView extends BorderPane {
|
||||
managedProperty().bind(fileTable.visibleProperty());
|
||||
}
|
||||
|
||||
private void populateFileDetailGrid(FileEntry fileEntry) {
|
||||
fileDetailGrid.getChildren().clear();
|
||||
fileDetailGrid.setAlignment(Pos.CENTER_LEFT);
|
||||
fileDetailGrid.setHgap(10);
|
||||
fileDetailGrid.setVgap(10);
|
||||
int row = 0;
|
||||
SimpleListProperty<Node> editableFields = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
Button applyButton = new Button("Apply");
|
||||
for (DisplayColumn displayColumn : fileEntry.getFileStore().getDisplayColumns()) {
|
||||
if (!displayColumn.supports(DisplayMode.DETAIL)) continue;
|
||||
String text = displayColumn.getTitle();
|
||||
if (!text.endsWith(":")) text += ":";
|
||||
Label label = new Label(text);
|
||||
label.setStyle("-fx-font-weight: bold;");
|
||||
if (displayColumn.isEditable()) {
|
||||
if (displayColumn.getOptions().isEmpty()) {
|
||||
TextField field = new TextField(displayColumn.getDisplayText(fileEntry));
|
||||
List<UnaryOperator<TextFormatter.Change>> filters = new ArrayList<>();
|
||||
if (displayColumn.getMaxLength() > 0) {
|
||||
filters.add(new FixedLengthFilter(displayColumn.getMaxLength()));
|
||||
}
|
||||
if (displayColumn.getValidInputPattern() != null) {
|
||||
filters.add(new CharacterRestrictionFilter(displayColumn.getValidCharacters()));
|
||||
}
|
||||
field.setTextFormatter(new TextFormatter<>(new CompositeFilter(filters)));
|
||||
field.textProperty().addListener((_, _, newText) -> {
|
||||
if (displayColumn.getValidInputPattern() != null
|
||||
&& !displayColumn.getValidInputPattern().matcher(newText).matches()) {
|
||||
field.setStyle("-fx-border-color: red;");
|
||||
}
|
||||
else {
|
||||
field.setStyle("");
|
||||
}
|
||||
});
|
||||
field.setUserData(displayColumn);
|
||||
fileDetailGrid.addRow(row++, label, field);
|
||||
editableFields.add(field);
|
||||
}
|
||||
else {
|
||||
ComboBox<ColumnOption> combobox = new ComboBox<>();
|
||||
combobox.setUserData(displayColumn);
|
||||
combobox.getItems().addAll(displayColumn.getOptions());
|
||||
combobox.getSelectionModel().select(displayColumn.getOptions().stream()
|
||||
.filter(o -> Objects.equals(o.value(), displayColumn.getValue(fileEntry)))
|
||||
.findAny().orElse(displayColumn.getOptions().getFirst()));
|
||||
combobox.valueProperty().addListener((_, _, newValue) -> {
|
||||
combobox.setStyle(newValue == null ? "-fx-border-color: red;" : "");
|
||||
});
|
||||
combobox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ColumnOption option) {
|
||||
if (option == null) return "";
|
||||
return String.format("%s - %s", option.value(), option.description());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnOption fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
fileDetailGrid.addRow(row++, label, combobox);
|
||||
editableFields.add(combobox);
|
||||
}
|
||||
}
|
||||
else {
|
||||
fileDetailGrid.addRow(row++, label, new Label(displayColumn.getDisplayText(fileEntry)));
|
||||
}
|
||||
}
|
||||
if (!editableFields.isEmpty()) {
|
||||
// Calculate if it's valid and then negate it. This makes one invalid disable the button.
|
||||
// We have to handle every control created, however. :shrug:
|
||||
applyButton.disableProperty().bind(
|
||||
editableFields.stream().map(node -> switch (node) {
|
||||
// These define the VALID state
|
||||
case TextField textField -> textField.textProperty().isNotEmpty()
|
||||
.and(textField.styleProperty().isEmpty());
|
||||
case ComboBox<?> comboBox -> comboBox.getSelectionModel().selectedItemProperty().isNotNull()
|
||||
.and(comboBox.styleProperty().isEmpty());
|
||||
default -> throw new IllegalStateException("Unexpected value: " + node);
|
||||
}).reduce(Bindings::and).map(BooleanBinding::not).orElseThrow());
|
||||
applyButton.setOnAction(event -> {
|
||||
for (Node node : editableFields) {
|
||||
DisplayColumn column = (DisplayColumn) node.getUserData();
|
||||
switch (node) {
|
||||
case TextField textField -> {
|
||||
column.setValue(fileEntry, textField.getText());
|
||||
}
|
||||
case ComboBox<?> comboBox -> {
|
||||
ColumnOption option = (ColumnOption) comboBox.getSelectionModel().getSelectedItem();
|
||||
column.setValue(fileEntry, option.value());
|
||||
}
|
||||
default -> { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
fileStoreWindow.addChange();
|
||||
});
|
||||
fileDetailGrid.addRow(row++, applyButton);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDragDroppedHandler(DragEvent event) {
|
||||
Dragboard db = event.getDragboard();
|
||||
boolean success = false;
|
||||
@@ -258,7 +396,7 @@ public class FileView extends BorderPane {
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
FxUtils.showErrorDialog("Unable to drop files", e);
|
||||
}
|
||||
event.setDropCompleted(success);
|
||||
@@ -383,7 +521,7 @@ public class FileView extends BorderPane {
|
||||
public void clear() {
|
||||
directoryPath.clear();
|
||||
selectedDirectory.set(null);
|
||||
listingMode.setValue(DisplayColumn.Mode.NATIVE);
|
||||
listingMode.setValue(DisplayMode.NATIVE);
|
||||
deletedFilesToggleButton.setSelected(false);
|
||||
//createNewFileTable();
|
||||
fileTable.getSelectionModel().clearSelection();
|
||||
@@ -392,11 +530,11 @@ public class FileView extends BorderPane {
|
||||
}
|
||||
|
||||
private void selectNativeView() {
|
||||
listingMode.set(DisplayColumn.Mode.NATIVE);
|
||||
listingMode.set(DisplayMode.NATIVE);
|
||||
}
|
||||
|
||||
private void selectDetailView() {
|
||||
listingMode.set(DisplayColumn.Mode.DETAIL);
|
||||
listingMode.set(DisplayMode.DETAIL);
|
||||
}
|
||||
|
||||
private void populateDiskRows() {
|
||||
@@ -416,29 +554,42 @@ public class FileView extends BorderPane {
|
||||
boolean editable = false;
|
||||
List<TableColumn<FileEntry,?>> tableColumns = new ArrayList<>();
|
||||
for (final DisplayColumn displayColumn : displayColumns) {
|
||||
TableColumn<FileEntry,?> column = switch (displayColumn.dataType()) {
|
||||
TableColumn<FileEntry,?> column = switch (displayColumn.getDataType()) {
|
||||
case STRING -> {
|
||||
TableColumn<FileEntry,String> stringColumn = new TableColumn<>(displayColumn.headerText());
|
||||
if (displayColumn.editInline()) {
|
||||
stringColumn.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
TableColumn<FileEntry,String> stringColumn = new TableColumn<>(displayColumn.getTitle());
|
||||
stringColumn.setCellValueFactory(cell ->
|
||||
new SimpleStringProperty(displayColumn.getDisplayText(cell.getValue())));
|
||||
if (displayColumn.canEditInLine()) {
|
||||
stringColumn.setCellFactory(TextFieldTableCell.forTableColumn(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(String object) {
|
||||
if (object == null) {
|
||||
return "";
|
||||
}
|
||||
if (displayColumn.getValidCharacters() != null) {
|
||||
object = displayColumn.getValidCharacters().map(object);
|
||||
}
|
||||
if (displayColumn.getMaxLength() > 0 && object.length() > displayColumn.getMaxLength()) {
|
||||
object = object.substring(0, displayColumn.getMaxLength());
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fromString(String string) {
|
||||
return string;
|
||||
}
|
||||
}));
|
||||
stringColumn.setEditable(true);
|
||||
stringColumn.setCellValueFactory(cell -> {
|
||||
SimpleStringProperty property = new SimpleStringProperty(displayColumn.formatAsText(cell.getValue()));
|
||||
property.addListener((_, _, newValue) -> {
|
||||
displayColumn.setValueFn().accept(cell.getValue(), newValue);
|
||||
fileStoreWindow.addChange();
|
||||
});
|
||||
return property;
|
||||
stringColumn.setOnEditCommit(event -> {
|
||||
displayColumn.setValue(event.getRowValue(), event.getNewValue());
|
||||
});
|
||||
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());
|
||||
case INTEGER,REAL -> {
|
||||
TableColumn<FileEntry,Number> numberColumn = new TableColumn<>(displayColumn.getTitle());
|
||||
numberColumn.setCellFactory(c -> new TableCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Number item, boolean empty) {
|
||||
@@ -446,35 +597,31 @@ public class FileView extends BorderPane {
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(String.format(displayColumn.fmt(), item));
|
||||
setText(displayColumn.formatObject(item));
|
||||
}
|
||||
}
|
||||
});
|
||||
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;
|
||||
if (displayColumn.getDataType() == ColumnDataType.REAL) {
|
||||
numberColumn.setCellValueFactory(cell -> {
|
||||
Number number = (Number) displayColumn.getValue(cell.getValue());
|
||||
return new SimpleDoubleProperty(number.doubleValue());
|
||||
});
|
||||
}
|
||||
else {
|
||||
numberColumn.setCellValueFactory(cell -> {
|
||||
Number number = (Number) displayColumn.getValue(cell.getValue());
|
||||
return new SimpleLongProperty(number.longValue());
|
||||
});
|
||||
}
|
||||
yield numberColumn;
|
||||
}
|
||||
};
|
||||
column.setUserData(displayColumn);
|
||||
column.visibleProperty().setValue(displayColumn.supports(listingMode.get()));
|
||||
if (displayColumn.alignment() == DisplayColumn.Alignment.RIGHT) {
|
||||
column.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
} else if (displayColumn.alignment() == DisplayColumn.Alignment.CENTER) {
|
||||
column.setStyle("-fx-alignment: CENTER;");
|
||||
} else {
|
||||
column.setStyle("-fx-alignment: CENTER-LEFT;");
|
||||
switch (displayColumn.getAlignment()) {
|
||||
case RIGHT -> column.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
case CENTER -> column.setStyle("-fx-alignment: CENTER;");
|
||||
case LEFT -> column.setStyle("-fx-alignment: CENTER-LEFT;");
|
||||
}
|
||||
tableColumns.add(column);
|
||||
}
|
||||
@@ -497,6 +644,9 @@ public class FileView extends BorderPane {
|
||||
fileStoreWindow.addChange();
|
||||
}
|
||||
}
|
||||
else if (event.isShortcutDown() && event.getCode() == KeyCode.I) {
|
||||
showingDetailProperty.set(!showingDetailProperty.get());
|
||||
}
|
||||
});
|
||||
|
||||
List<? extends FileEntry> rows = directory.getFiles().stream()
|
||||
@@ -510,8 +660,8 @@ public class FileView extends BorderPane {
|
||||
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);
|
||||
nativeToolButton.setSelected(listingMode.get() == DisplayMode.NATIVE);
|
||||
detailToolButton.setSelected(listingMode.get() == DisplayMode.DETAIL);
|
||||
}
|
||||
|
||||
private void toggleDeletedFiles() {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package org.applecommander.javafx;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -48,6 +49,7 @@ public class FxUtils {
|
||||
Button button = new Button();
|
||||
button.setGraphic(vbox);
|
||||
button.setOnAction(eventHandler);
|
||||
button.setFocusTraversable(false);
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -64,6 +66,7 @@ public class FxUtils {
|
||||
button.setGraphic(vbox);
|
||||
button.setOnAction(eventHandler);
|
||||
button.setToggleGroup(toggleGroup);
|
||||
button.setFocusTraversable(false);
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -78,6 +81,7 @@ public class FxUtils {
|
||||
ToggleButton button = new ToggleButton();
|
||||
button.setGraphic(vbox);
|
||||
button.setOnAction(eventHandler);
|
||||
button.setFocusTraversable(false);
|
||||
button.selectedProperty().addListener((_, _, _) -> {
|
||||
String imageUrl = button.isSelected() ? urlOn : urlOff;
|
||||
imageView.setImage(new Image(imageUrl(imageUrl)));
|
||||
@@ -85,6 +89,25 @@ public class FxUtils {
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Button createSmallToggleButton(String urlOff, String urlOn, BooleanProperty toggleState) {
|
||||
toggleState.setValue(false);
|
||||
ImageView imageView = new ImageView(imageUrl(urlOff));
|
||||
imageView.setFitHeight(16);
|
||||
imageView.setFitWidth(16);
|
||||
imageView.setPreserveRatio(true);
|
||||
Button button = new Button();
|
||||
button.setFocusTraversable(false);
|
||||
button.setGraphic(imageView);
|
||||
button.setOnAction(event -> {
|
||||
toggleState.setValue(!toggleState.getValue());
|
||||
});
|
||||
toggleState.addListener((_, _, _) -> {
|
||||
String imageUrl = toggleState.get() ? urlOn : urlOff;
|
||||
imageView.setImage(new Image(imageUrl(imageUrl)));
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
public static String imageUrl(String imageName) {
|
||||
return Objects.requireNonNull(FxUtils.class.getResource("/images/" + imageName)).toExternalForm();
|
||||
}
|
||||
@@ -103,7 +126,9 @@ public class FxUtils {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
t.printStackTrace(pw);
|
||||
alert.setContentText(sw.toString());
|
||||
TextArea textArea = new TextArea(sw.toString());
|
||||
textArea.setEditable(false);
|
||||
alert.getDialogPane().setExpandableContent(textArea);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.javafx.filter;
|
||||
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import org.applecommander.ui.CharacterMapper;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
public class CharacterRestrictionFilter implements UnaryOperator<TextFormatter.Change> {
|
||||
private final CharacterMapper characterMapper;
|
||||
|
||||
public CharacterRestrictionFilter(CharacterMapper characterMapper) {
|
||||
this.characterMapper = characterMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextFormatter.Change apply(TextFormatter.Change change) {
|
||||
if (change.isAdded()) {
|
||||
change.setText(characterMapper.map(change.getText()));
|
||||
}
|
||||
return change;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.javafx.filter;
|
||||
|
||||
import javafx.scene.control.TextFormatter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
public class CompositeFilter implements UnaryOperator<TextFormatter.Change> {
|
||||
private final List<UnaryOperator<TextFormatter.Change>> filters;
|
||||
|
||||
public CompositeFilter(List<UnaryOperator<TextFormatter.Change>> filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextFormatter.Change apply(TextFormatter.Change change) {
|
||||
for (UnaryOperator<TextFormatter.Change> filter : filters) {
|
||||
change = filter.apply(change);
|
||||
if (change == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return change;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.javafx.filter;
|
||||
|
||||
import javafx.scene.control.TextFormatter;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
// See: https://www.reddit.com/r/JavaFX/comments/qd5uds/how_do_you_limit_the_character_input_in_a/
|
||||
public class FixedLengthFilter implements UnaryOperator<TextFormatter.Change> {
|
||||
private final int maxLength;
|
||||
|
||||
public FixedLengthFilter(int maxLength) {
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextFormatter.Change apply(TextFormatter.Change change) {
|
||||
if (change.getControlNewText().length() > maxLength) {
|
||||
return null;
|
||||
}
|
||||
return change;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
+18
-19
@@ -41,6 +41,8 @@ import org.applecommander.device.TrackSectorDevice;
|
||||
import org.applecommander.filestore.*;
|
||||
import org.applecommander.filestore.FileEntry;
|
||||
import org.applecommander.transfer.ProdosAttributes;
|
||||
import org.applecommander.ui.DisplayColumn;
|
||||
import org.applecommander.ui.DisplayMode;
|
||||
import org.applecommander.usage.BlockUsage;
|
||||
import org.applecommander.usage.DiskUsage;
|
||||
import org.applecommander.usage.DiskUsage.UsageType;
|
||||
@@ -145,35 +147,32 @@ public class DiskFileStoreAdapter implements FileStore {
|
||||
/// old API pre-formats everything as a String.
|
||||
@Override
|
||||
public List<DisplayColumn> getDisplayColumns() {
|
||||
List<DisplayColumn> columns = new ArrayList<>();
|
||||
DisplayColumn.Builder<DiskFileEntryAdapter> builder = DisplayColumn.builder(DiskFileEntryAdapter.class);
|
||||
for (int displayMode : List.of(FormattedDisk.FILE_DISPLAY_NATIVE,
|
||||
FormattedDisk.FILE_DISPLAY_DETAIL)) {
|
||||
List<FormattedDisk.FileColumnHeader> headers = disk.getFileColumnHeaders(displayMode);
|
||||
DisplayColumn.Mode mode = DisplayColumn.Mode.DETAIL;
|
||||
DisplayMode mode = DisplayMode.DETAIL;
|
||||
if (displayMode == FormattedDisk.FILE_DISPLAY_NATIVE) {
|
||||
mode = DisplayColumn.Mode.NATIVE;
|
||||
};
|
||||
mode = DisplayMode.NATIVE;
|
||||
}
|
||||
for (int i = 0; i<headers.size(); i++) {
|
||||
var header = headers.get(i);
|
||||
DisplayColumn.Alignment alignment = switch(header.getAlignment()) {
|
||||
case FormattedDisk.FileColumnHeader.ALIGN_CENTER -> DisplayColumn.Alignment.CENTER;
|
||||
case FormattedDisk.FileColumnHeader.ALIGN_RIGHT -> DisplayColumn.Alignment.RIGHT;
|
||||
default -> DisplayColumn.Alignment.LEFT;
|
||||
};
|
||||
String title = header.getTitle();
|
||||
if (title == null || title.trim().isEmpty()) {
|
||||
title = "Locked?";
|
||||
}
|
||||
final int headerIndex = i;
|
||||
Function<FileEntry,String> mappingFn = entry -> {
|
||||
// TODO investigate to see if generics work across these interfaces
|
||||
if (entry instanceof DiskFileEntryAdapter fileEntryAdapter) {
|
||||
return fileEntryAdapter.fileEntry.getFileColumnData(displayMode).get(headerIndex);
|
||||
}
|
||||
throw new RuntimeException("Unexpected file entry type: " + entry.getClass().getName());
|
||||
Function<DiskFileEntryAdapter,String> getValueFn = entry ->
|
||||
entry.fileEntry.getFileColumnData(displayMode).get(headerIndex);
|
||||
DisplayColumn.ColumnBuilder<DiskFileEntryAdapter> column = switch (header.getAlignment()) {
|
||||
case FormattedDisk.FileColumnHeader.ALIGN_CENTER -> builder.center(title);
|
||||
case FormattedDisk.FileColumnHeader.ALIGN_RIGHT -> builder.right(title);
|
||||
default -> builder.left(title);
|
||||
};
|
||||
DisplayColumn displayColumn = new DisplayColumn(header.getTitle(),
|
||||
alignment, DisplayColumn.DataType.STRING, mappingFn::apply, null, false, "%s", mode);
|
||||
columns.add(displayColumn);
|
||||
column.string(getValueFn).mode(mode).done();
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
return builder.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -55,6 +55,7 @@ module org.applecommander.api {
|
||||
exports org.applecommander.os.pascal;
|
||||
exports org.applecommander.source;
|
||||
exports org.applecommander.transfer;
|
||||
exports org.applecommander.ui;
|
||||
exports org.applecommander.usage;
|
||||
exports org.applecommander.util;
|
||||
|
||||
|
||||
+7
-3
@@ -20,8 +20,12 @@
|
||||
package org.applecommander.archive.aaf;
|
||||
|
||||
import org.applecommander.capability.Capability;
|
||||
import org.applecommander.filestore.*;
|
||||
import org.applecommander.filestore.Directory;
|
||||
import org.applecommander.filestore.FileEntry;
|
||||
import org.applecommander.filestore.FileStore;
|
||||
import org.applecommander.filestore.GenericDirectory;
|
||||
import org.applecommander.source.Source;
|
||||
import org.applecommander.ui.DisplayColumn;
|
||||
import org.applecommander.util.Container;
|
||||
import org.applecommander.util.InformationGroup;
|
||||
|
||||
@@ -94,8 +98,8 @@ public class AppleArchiveFormatFileStore implements FileStore {
|
||||
@Override
|
||||
public List<DisplayColumn> getDisplayColumns() {
|
||||
return DisplayColumn.builder(AppleArchiveFormatFileEntry.class)
|
||||
.addStringField("Name", AppleArchiveFormatFileEntry::getName)
|
||||
.addLongField("Size", AppleArchiveFormatFileEntry::getSize)
|
||||
.left("Name").string(AppleArchiveFormatFileEntry::getName).done()
|
||||
.right("Size").integer(AppleArchiveFormatFileEntry::getSize, "%,d").done()
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
+21
-18
@@ -20,12 +20,15 @@
|
||||
package org.applecommander.archive.shrinkit;
|
||||
|
||||
import org.applecommander.capability.Capability;
|
||||
import org.applecommander.filestore.*;
|
||||
import org.applecommander.filestore.DisplayColumn.Alignment;
|
||||
import org.applecommander.filestore.DisplayColumn.Mode;
|
||||
import org.applecommander.filestore.Directory;
|
||||
import org.applecommander.filestore.FileEntry;
|
||||
import org.applecommander.filestore.FileStore;
|
||||
import org.applecommander.filestore.GenericDirectory;
|
||||
import org.applecommander.shrinkit.MasterHeaderBlock;
|
||||
import org.applecommander.shrinkit.NuFileArchive;
|
||||
import org.applecommander.source.Source;
|
||||
import org.applecommander.ui.DisplayColumn;
|
||||
import org.applecommander.ui.DisplayMode;
|
||||
import org.applecommander.util.Container;
|
||||
import org.applecommander.util.InformationGroup;
|
||||
|
||||
@@ -78,21 +81,21 @@ public class ShrinkitFileStore implements FileStore {
|
||||
@Override
|
||||
public List<DisplayColumn> getDisplayColumns() {
|
||||
return DisplayColumn.builder(ShrinkitFileEntry.class)
|
||||
.addStringField("Name", ShrinkitFileEntry::getName)
|
||||
.addStringField("Sys. Id", ShrinkitFileEntry::getFileSysIdText, Mode.DETAIL)
|
||||
.addIntField("File Sys. Info", ShrinkitFileEntry::getFileSysInfo, "$%04X", Mode.DETAIL)
|
||||
.addStringField("Access", Alignment.CENTER, ShrinkitFileEntry::getAccessText, Mode.DETAIL)
|
||||
.addStringField("Kind", ShrinkitFileEntry::getStorageTypeText)
|
||||
.addStringField("Type", Alignment.CENTER, ShrinkitFileEntry::getFileTypeText)
|
||||
.addLongField("Aux. Type", ShrinkitFileEntry::getExtraType, "$%04X")
|
||||
.addDateField("Archived", ShrinkitFileEntry::getArchiveWhen)
|
||||
.addDateField("Created", ShrinkitFileEntry::getCreateWhen, Mode.DETAIL)
|
||||
.addDateField("Modified", ShrinkitFileEntry::getModWhen, Mode.DETAIL)
|
||||
.addStringField("Format", ShrinkitFileEntry::getThreadFormatText)
|
||||
.addPercentField("Size", ShrinkitFileEntry::getCompressedSize, ShrinkitFileEntry::getSize, "%2.0f%%")
|
||||
.addLongField("Un-Length", ShrinkitFileEntry::getSize)
|
||||
.addIntField("Data CRC", ShrinkitFileEntry::getDataForkCrc, "$%04X", Mode.DETAIL)
|
||||
.addIntField("Resource CRC", ShrinkitFileEntry::getResourceForkCrc, "$%04X", Mode.DETAIL)
|
||||
.left("Name").string(ShrinkitFileEntry::getName).done()
|
||||
.left("Sys. Id").string(ShrinkitFileEntry::getFileSysIdText).mode(DisplayMode.DETAIL).done()
|
||||
.right("File Sys. Info").integer(ShrinkitFileEntry::getFileSysInfo, "$%04X").mode(DisplayMode.DETAIL).done()
|
||||
.center("Access").string(ShrinkitFileEntry::getAccessText).mode(DisplayMode.DETAIL).done()
|
||||
.left("Kind").string(ShrinkitFileEntry::getStorageTypeText).done()
|
||||
.center("Type").string(ShrinkitFileEntry::getFileTypeText).done()
|
||||
.right("Aux. Type").integer(ShrinkitFileEntry::getExtraType, "$%04X").done()
|
||||
.center("Archived").date(ShrinkitFileEntry::getArchiveWhen).done()
|
||||
.center("Created").date(ShrinkitFileEntry::getCreateWhen).mode(DisplayMode.DETAIL).done()
|
||||
.center("Modified").date(ShrinkitFileEntry::getModWhen).mode(DisplayMode.DETAIL).done()
|
||||
.left("Format").string(ShrinkitFileEntry::getThreadFormatText).done()
|
||||
.right("Size").percent(ShrinkitFileEntry::getCompressedSize, ShrinkitFileEntry::getSize, "%2.0f%%").done()
|
||||
.right("Un-Length").integer(ShrinkitFileEntry::getSize, "%,d").done()
|
||||
.right("Data CRC").integer(ShrinkitFileEntry::getDataForkCrc, "$%04X").mode(DisplayMode.DETAIL).done()
|
||||
.right("Resource CRC").integer(ShrinkitFileEntry::getResourceForkCrc, "$%04X").mode(DisplayMode.DETAIL).done()
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
package org.applecommander.archive.zip;
|
||||
|
||||
import org.applecommander.capability.Capability;
|
||||
import org.applecommander.filestore.DisplayColumn;
|
||||
import org.applecommander.filestore.DisplayColumn.Mode;
|
||||
import org.applecommander.filestore.FileEntry;
|
||||
import org.applecommander.filestore.FileStore;
|
||||
import org.applecommander.source.Source;
|
||||
import org.applecommander.ui.DisplayColumn;
|
||||
import org.applecommander.ui.DisplayMode;
|
||||
import org.applecommander.util.Container;
|
||||
import org.applecommander.util.DataBuffer;
|
||||
import org.applecommander.util.InformationGroup;
|
||||
@@ -40,6 +40,7 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.applecommander.ui.DisplayColumn.builder;
|
||||
import static org.applecommander.util.Shorthand.copy;
|
||||
|
||||
public class ZipFileStore implements FileStore {
|
||||
@@ -90,16 +91,16 @@ public class ZipFileStore implements FileStore {
|
||||
|
||||
@Override
|
||||
public List<DisplayColumn> getDisplayColumns() {
|
||||
return DisplayColumn.builder(ZipFileEntry.class)
|
||||
.addLongField("Length", FileEntry::getSize)
|
||||
.addStringField("Method", ZipFileEntry::getMethodName, Mode.DETAIL)
|
||||
.addLongField("Size", ZipFileEntry::getCompressedSize, Mode.DETAIL)
|
||||
.addPercentField("Ratio", ZipFileEntry::getCompressedSize, ZipFileEntry::getSize, "%2.0f%%", Mode.DETAIL)
|
||||
.addFileTimeField("Creation", ZipFileEntry::getCreationTime)
|
||||
.addFileTimeField("Last Access", ZipFileEntry::getLastAccessTime, Mode.DETAIL)
|
||||
.addFileTimeField("Last Modified", ZipFileEntry::getLastModifiedTime)
|
||||
.addLongField("CRC-32", ZipFileEntry::getCrc, "%08x", Mode.DETAIL)
|
||||
.addStringField("Name", FileEntry::getName)
|
||||
return builder(ZipFileEntry.class)
|
||||
.right("Length").integer(FileEntry::getSize, "%,d").done()
|
||||
.left("Method").string(ZipFileEntry::getMethodName).mode(DisplayMode.DETAIL).done()
|
||||
.right("Size").integer(ZipFileEntry::getCompressedSize, "%,d").mode(DisplayMode.DETAIL).done()
|
||||
.right("Ratio").percent(ZipFileEntry::getCompressedSize, ZipFileEntry::getSize, "%2.0f%%").mode(DisplayMode.DETAIL).done()
|
||||
.center("Creation").fileTime(ZipFileEntry::getCreationTime).done()
|
||||
.center("Last Access").fileTime(ZipFileEntry::getLastAccessTime).mode(DisplayMode.DETAIL).done()
|
||||
.center("Last Modified").fileTime(ZipFileEntry::getLastModifiedTime).done()
|
||||
.right("CRC-32").integer(ZipFileEntry::getCrc, "%08x").mode(DisplayMode.DETAIL).done()
|
||||
.left("Name").string(FileEntry::getName).editInLine().done()
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.filestore;
|
||||
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
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, DataType dataType,
|
||||
Function<FileEntry,Object> getValueFn, BiConsumer<FileEntry,Object> setValueFn,
|
||||
boolean editInline, String fmt, Mode... modes) {
|
||||
|
||||
/// Indicates if this DisplayColumn supports the mode. In this manner, the columns
|
||||
/// can be filtered out. Or it can be ignored to display everything.
|
||||
public boolean supports(Mode mode) {
|
||||
return Set.of(modes).contains(mode);
|
||||
}
|
||||
|
||||
/// Helper method to convert the column into a formatted string for display.
|
||||
public String formatAsText(FileEntry fileEntry) {
|
||||
return String.format(fmt, getValueFn.apply(fileEntry));
|
||||
}
|
||||
|
||||
/// Indicates the display mode to help filter columns to users' preference.
|
||||
public enum Mode {
|
||||
NATIVE,
|
||||
DETAIL
|
||||
}
|
||||
|
||||
/// Indicates how this column should be aligned.
|
||||
public enum Alignment {
|
||||
LEFT,
|
||||
CENTER,
|
||||
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);
|
||||
}
|
||||
public static class Builder<T extends FileEntry> {
|
||||
private static final DateTimeFormatter FILE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss");
|
||||
private final Class<T> clazz;
|
||||
private final List<DisplayColumn> columns = new ArrayList<>();
|
||||
|
||||
private Builder(Class<T> clazz) {
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
public List<DisplayColumn> toList() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
/// A helper method that allows a short-cut of 0 modes meaning ALL modes.
|
||||
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, dataType, getValueFn, setValueFn, editInline, fmt, modes));
|
||||
return this;
|
||||
}
|
||||
private Builder<T> add(String name, Alignment alignment, DataType dataType, Function<FileEntry,Object> getValueFn,
|
||||
String fmt, Mode... 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, 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, 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, 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, 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, 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, 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, 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, DataType.STRING, entry -> formatDate(entry, valueFn), "%s", modes);
|
||||
}
|
||||
|
||||
private <S> S convert(FileEntry fileEntry, Function<T,S> valueFn) {
|
||||
T typedFileEntry = clazz.cast(fileEntry);
|
||||
return valueFn.apply(typedFileEntry);
|
||||
}
|
||||
private <S> void convert(FileEntry fileEntry, BiConsumer<T,S> setValueFn, Class<S> valueClass, Object value) {
|
||||
T typedFileEntry = clazz.cast(fileEntry);
|
||||
S typedValue = valueClass.cast(value);
|
||||
setValueFn.accept(typedFileEntry, typedValue);
|
||||
}
|
||||
private double percentage(FileEntry fileEntry, Function<T,Number> numeratorFn, Function<T,Number> denominatorFn) {
|
||||
T typedFileEntry = clazz.cast(fileEntry);
|
||||
Number numerator = numeratorFn.apply(typedFileEntry);
|
||||
Number denominator = denominatorFn.apply(typedFileEntry);
|
||||
return (1.0 - numerator.doubleValue() / denominator.doubleValue()) * 100.0;
|
||||
}
|
||||
private String formatFileTime(FileEntry fileEntry, Function<T,FileTime> valueFn) {
|
||||
T typedFileEntry = clazz.cast(fileEntry);
|
||||
// https://mkyong.com/java/how-to-format-filetime-in-java/
|
||||
FileTime fileTime = valueFn.apply(typedFileEntry);
|
||||
if (fileTime == null) {
|
||||
return "- No Date -";
|
||||
}
|
||||
LocalDateTime localDateTime = fileTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
return localDateTime.format(FILE_TIME_FORMATTER);
|
||||
}
|
||||
private String formatDate(FileEntry fileEntry, Function<T, Date> valueFn) {
|
||||
T typedFileEntry = clazz.cast(fileEntry);
|
||||
Date date = valueFn.apply(typedFileEntry);
|
||||
if (date == null) {
|
||||
return "- No Date -";
|
||||
}
|
||||
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
return localDateTime.format(FILE_TIME_FORMATTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.applecommander.filestore;
|
||||
|
||||
import org.applecommander.capability.CapabilityProvider;
|
||||
import org.applecommander.ui.DisplayColumn;
|
||||
import org.applecommander.util.Container;
|
||||
import org.applecommander.util.InformationProvider;
|
||||
|
||||
|
||||
@@ -21,9 +21,10 @@ package org.applecommander.os.gamedos;
|
||||
|
||||
import org.applecommander.capability.Capability;
|
||||
import org.applecommander.device.TrackSectorDevice;
|
||||
import org.applecommander.filestore.DisplayColumn;
|
||||
import org.applecommander.filestore.FileEntry;
|
||||
import org.applecommander.filestore.FileStore;
|
||||
import org.applecommander.ui.DisplayColumn;
|
||||
import org.applecommander.ui.DisplayMode;
|
||||
import org.applecommander.usage.DiskUsage.UsageType;
|
||||
import org.applecommander.usage.SectorUsage;
|
||||
import org.applecommander.util.Container;
|
||||
@@ -33,8 +34,6 @@ import org.applecommander.util.InformationGroup;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.applecommander.filestore.DisplayColumn.Mode;
|
||||
|
||||
public class GamedosFileStore implements FileStore, GamedosConstants {
|
||||
private final TrackSectorDevice device;
|
||||
private final GamedosDirectory rootDirectory;
|
||||
@@ -108,13 +107,22 @@ public class GamedosFileStore implements FileStore, GamedosConstants {
|
||||
@Override
|
||||
public List<DisplayColumn> getDisplayColumns() {
|
||||
return DisplayColumn.builder(GamedosFileEntry.class)
|
||||
.addStringField("Type", GamedosFileEntry::getFiletype, GamedosFileEntry::setFiletype, false)
|
||||
.addStringField("Name", GamedosFileEntry::getName, GamedosFileEntry::setName, true)
|
||||
.addIntField("Meta", GamedosFileEntry::getMeta, "$%04X")
|
||||
.addIntField("First Track", GamedosFileEntry::getFirstTrack, Mode.DETAIL)
|
||||
.addIntField("First Sector", GamedosFileEntry::getFirstSector, Mode.DETAIL)
|
||||
.addIntField("Sector Count", GamedosFileEntry::getSectorCount, Mode.DETAIL)
|
||||
.addLongField("Size", GamedosFileEntry::getSize)
|
||||
.left("Type").string(GamedosFileEntry::getFiletype, GamedosFileEntry::setFiletype)
|
||||
.option("A", "Applesoft BASIC")
|
||||
.option("B", "Binary")
|
||||
.option("I", "Integer BASIC")
|
||||
.option("P", "Picture")
|
||||
.option("S", "System")
|
||||
.option("T", "Text")
|
||||
.done()
|
||||
.left("Name").string(GamedosFileEntry::getName, GamedosFileEntry::setName)
|
||||
.editInLine().maxLength(26).done()
|
||||
.right("Meta").integer(GamedosFileEntry::getMeta, "$%04X", GamedosFileEntry::setMeta)
|
||||
.restrictToHexValues().done()
|
||||
.right("First Track").integer(GamedosFileEntry::getFirstTrack, "%d").mode(DisplayMode.DETAIL).done()
|
||||
.right("First Sector").integer(GamedosFileEntry::getFirstSector, "%d").mode(DisplayMode.DETAIL).done()
|
||||
.right("Sector Count").integer(GamedosFileEntry::getSectorCount, "%d").mode(DisplayMode.DETAIL).done()
|
||||
.right("Size").integer(GamedosFileEntry::getSize, "%,d").done()
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.ui;
|
||||
|
||||
/// `CharacterMapper` utility class that is used to convert an input string into a set of
|
||||
/// valid characters. This can handle both lower-case to upper-case mappings and
|
||||
/// restricting input characters or doing things like mapping a ' ' to a '.'.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```
|
||||
/// CharacterMapper hex = CharacterMapper.builder()
|
||||
/// .mapRange('a', 'f', true)
|
||||
/// .mapRange('A', 'F', false)
|
||||
/// .mapRange('0', '9', false)
|
||||
/// .map('$', '$')
|
||||
/// .build();
|
||||
///
|
||||
/// System.out.println(hex.map(">>> $12af <<<"));
|
||||
/// ```
|
||||
/// ... yields a `$12AF` output.
|
||||
public class CharacterMapper {
|
||||
private final String fromChars;
|
||||
private final String toChars;
|
||||
|
||||
private CharacterMapper(String fromChars, String toChars) {
|
||||
this.fromChars = fromChars;
|
||||
this.toChars = toChars;
|
||||
}
|
||||
|
||||
public String map(String inputString) {
|
||||
// No restrictions
|
||||
if (fromChars.isBlank() && toChars.isBlank()) {
|
||||
return inputString;
|
||||
}
|
||||
// Copy valid characters ("fromChars") over to the modified string ("toChars")
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (char ch : inputString.toCharArray()) {
|
||||
int pos = fromChars.indexOf(ch);
|
||||
if (pos != -1) {
|
||||
sb.append(toChars.charAt(pos));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
public static class Builder {
|
||||
private final StringBuilder fromChars = new StringBuilder();
|
||||
private final StringBuilder toChars = new StringBuilder();
|
||||
|
||||
/// Add a range of characters
|
||||
public Builder mapRange(char first, char last, boolean forceUppercase) {
|
||||
StringBuilder string = new StringBuilder();
|
||||
for (char ch = first; ch <= last; ch++) string.append(ch);
|
||||
String alpha = string.toString();
|
||||
|
||||
fromChars.append(alpha);
|
||||
if (forceUppercase) alpha = alpha.toUpperCase();
|
||||
toChars.append(alpha);
|
||||
return this;
|
||||
}
|
||||
/// Add a single character mapping
|
||||
public Builder map(char from, char to) {
|
||||
fromChars.append(from);
|
||||
toChars.append(to);
|
||||
return this;
|
||||
}
|
||||
public CharacterMapper build() {
|
||||
if (fromChars.length() != toChars.length()) {
|
||||
throw new RuntimeException("developer error - character mappings don't match");
|
||||
}
|
||||
return new CharacterMapper(fromChars.toString(), toChars.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.ui;
|
||||
|
||||
/// Indicates how this column should be aligned.
|
||||
public enum ColumnAlignment {
|
||||
LEFT,
|
||||
CENTER,
|
||||
RIGHT
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.ui;
|
||||
|
||||
public enum ColumnDataType {
|
||||
/// Represents strings or things that are converted to strings.
|
||||
STRING,
|
||||
/// Represents numbers without decimal places: `byte`, `short`, `int`, `long`.
|
||||
INTEGER,
|
||||
/// Represents numbers _with_ decimal places: `float`, `double`.
|
||||
REAL
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.ui;
|
||||
|
||||
public record ColumnOption(String value, String description) {
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.ui;
|
||||
|
||||
import org.applecommander.filestore.FileEntry;
|
||||
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/// A `DisplayColumn` supports formatted output from various `FileStore`s in a
|
||||
/// dynamic fashion. The intent is to provide a flexible display interface
|
||||
/// across interfaces that does not rely on hard-coded implementations.
|
||||
///
|
||||
/// The `Builder` should be utilized to construct the `DisplayColumn`.
|
||||
///
|
||||
/// The read-only settings include:
|
||||
/// * `title` is the column title
|
||||
/// * `alignment` is the alignment of the column data
|
||||
/// * `mode` is the display mode that the column applies to (native, detail, or both)
|
||||
/// * `columnDataType` is the (simplified) data type
|
||||
/// * `getValueFn` is the method reference to get the column's value
|
||||
/// * `formatFn` is the function that takes the column's value and prepares it for display
|
||||
///
|
||||
/// The editable attributes are:
|
||||
/// * `setValueFn` is the method reference to set the value; note that the builder wraps
|
||||
/// the actual setter with a conversion function
|
||||
/// * `editInLine` indicates if this specific cell can be edited in a grid; at this time
|
||||
/// this is only used for the name field
|
||||
/// * `maxLength` is the maximum length of the input
|
||||
/// * `options` are the options available for selection
|
||||
/// * `validCharacters` is a `CharacterMapper` to transform input text
|
||||
/// * `validInputPattern` is a `Pattern` used to validate the input
|
||||
///
|
||||
public class DisplayColumn {
|
||||
private final String title;
|
||||
private final ColumnAlignment alignment;
|
||||
private final Set<DisplayMode> modes;
|
||||
private final ColumnDataType columnDataType;
|
||||
private final Function<FileEntry,Object> getValueFn;
|
||||
private final Function<Object,String> formatFn;
|
||||
// These are all editable attributes
|
||||
private final BiConsumer<FileEntry,String> setValueFn;
|
||||
private final boolean editable;
|
||||
private final boolean editInLine;
|
||||
private final int maxLength;
|
||||
private final List<ColumnOption> options;
|
||||
private final CharacterMapper validCharacters;
|
||||
private final Pattern validInputPattern;
|
||||
|
||||
private DisplayColumn(String title, ColumnAlignment alignment, Set<DisplayMode> modes, ColumnDataType columnDataType,
|
||||
Function<FileEntry,Object> getValueFn, Function<Object,String> formatFn, BiConsumer<FileEntry,String> setValueFn,
|
||||
boolean editInLine, int maxLength, List<ColumnOption> options,
|
||||
CharacterMapper validCharacters, Pattern validInputPattern) {
|
||||
Objects.requireNonNull(title);
|
||||
Objects.requireNonNull(alignment);
|
||||
Objects.requireNonNull(modes);
|
||||
Objects.requireNonNull(columnDataType);
|
||||
Objects.requireNonNull(getValueFn);
|
||||
// setValueFn is null when not editable
|
||||
Objects.requireNonNull(formatFn);
|
||||
Objects.requireNonNull(options);
|
||||
Objects.requireNonNull(validCharacters);
|
||||
// validInputPattern is null when not used
|
||||
this.title = title;
|
||||
this.alignment = alignment;
|
||||
this.modes = modes;
|
||||
this.columnDataType = columnDataType;
|
||||
this.getValueFn = getValueFn;
|
||||
this.formatFn = formatFn;
|
||||
|
||||
this.editable = setValueFn != null || editInLine || maxLength > 0 || !options.isEmpty() || validInputPattern != null;
|
||||
this.setValueFn = setValueFn;
|
||||
this.editInLine = editInLine;
|
||||
this.maxLength = maxLength;
|
||||
this.options = Collections.unmodifiableList(options);
|
||||
this.validCharacters = validCharacters;
|
||||
this.validInputPattern = validInputPattern;
|
||||
}
|
||||
|
||||
/// The column title.
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
/// Retrieve the column alignment.
|
||||
public ColumnAlignment getAlignment() {
|
||||
return alignment;
|
||||
}
|
||||
/// Indicates if this DisplayColumn supports the DisplayMode. In this manner, the columns
|
||||
/// can be filtered out. Or it can be ignored to display everything.
|
||||
public boolean supports(DisplayMode mode) {
|
||||
return modes.contains(mode);
|
||||
}
|
||||
/// Indicates the (generic) data type of this column.
|
||||
public ColumnDataType getDataType() {
|
||||
return columnDataType;
|
||||
}
|
||||
/// Get the column value for this FileEntry.
|
||||
public Object getValue(FileEntry fileEntry) {
|
||||
return getValueFn.apply(fileEntry);
|
||||
}
|
||||
/// Convert the column into a formatted string for display.
|
||||
public String getDisplayText(FileEntry fileEntry) {
|
||||
return formatObject(getValue(fileEntry));
|
||||
}
|
||||
/// Explicitly format the object. This, for instance, is used by JavaFX when it is managing the data types.
|
||||
public String formatObject(Object object) {
|
||||
return formatFn.apply(object);
|
||||
}
|
||||
public void setValue(FileEntry fileEntry, String value) {
|
||||
setValueFn.accept(fileEntry, value);
|
||||
}
|
||||
|
||||
/// Indicates if this field is editable
|
||||
public boolean isEditable() {
|
||||
return editable;
|
||||
}
|
||||
/// Indicates this column can be edited directly in the file viewer. Typically just the name.
|
||||
public boolean canEditInLine() {
|
||||
return editInLine;
|
||||
}
|
||||
/// Indicates the maximum length for the input. 0 means not applicable.
|
||||
public int getMaxLength() {
|
||||
return maxLength;
|
||||
}
|
||||
/// Get list of options (if any) when editing.
|
||||
public List<ColumnOption> getOptions() {
|
||||
return options;
|
||||
}
|
||||
/// Get the CharacterMapper that maps the input string into the valid set of characters.
|
||||
public CharacterMapper getValidCharacters() {
|
||||
return validCharacters;
|
||||
}
|
||||
/// Get the valid input regex. If input does not match this regex, it will be ignored.
|
||||
/// The intent is to be able to limit to numbers, or the valid filename characters.
|
||||
public Pattern getValidInputPattern() {
|
||||
return validInputPattern;
|
||||
}
|
||||
|
||||
public static <T extends FileEntry> Builder<T> builder(Class<T> fileEntryType) {
|
||||
return new Builder<>(fileEntryType);
|
||||
}
|
||||
|
||||
public static class Builder<T extends FileEntry> {
|
||||
private final Class<T> fileEntryType;
|
||||
private final List<DisplayColumn> columns = new ArrayList<>();
|
||||
|
||||
private Builder(Class<T> fileEntryType) {
|
||||
Objects.requireNonNull(fileEntryType);
|
||||
this.fileEntryType = fileEntryType;
|
||||
}
|
||||
|
||||
public ColumnBuilder<T> left(String title) {
|
||||
Objects.requireNonNull(title);
|
||||
return new ColumnBuilder<>(this, ColumnAlignment.LEFT, title);
|
||||
}
|
||||
public ColumnBuilder<T> center(String title) {
|
||||
Objects.requireNonNull(title);
|
||||
return new ColumnBuilder<>(this, ColumnAlignment.CENTER, title);
|
||||
}
|
||||
public ColumnBuilder<T> right(String title) {
|
||||
Objects.requireNonNull(title);
|
||||
return new ColumnBuilder<>(this, ColumnAlignment.RIGHT, title);
|
||||
}
|
||||
|
||||
public List<DisplayColumn> toList() {
|
||||
return columns;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ColumnBuilder<T extends FileEntry> {
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss");
|
||||
public static final Pattern VALID_HEX_NUMBER = Pattern.compile("^(?i)0x[0-9a-f]+|[$][0-9a-f]+|[0-9a-f]+H?$");
|
||||
|
||||
private final Builder<T> builder;
|
||||
private final ColumnAlignment alignment;
|
||||
private final String title;
|
||||
private ColumnDataType columnDataType;
|
||||
private Function<T, ?> getValueFn;
|
||||
private BiConsumer<FileEntry,String> setValueFn;
|
||||
private Function<Object,String> formatFn;
|
||||
private DisplayMode[] modes;
|
||||
private boolean editInLine = false;
|
||||
private int maxLength = 0;
|
||||
private final List<ColumnOption> options = new ArrayList<>();
|
||||
private CharacterMapper validCharacters;
|
||||
private Pattern validInputPattern;
|
||||
|
||||
private ColumnBuilder(Builder<T> builder, ColumnAlignment alignment, String title) {
|
||||
Objects.requireNonNull(builder);
|
||||
Objects.requireNonNull(alignment);
|
||||
Objects.requireNonNull(title);
|
||||
this.builder = builder;
|
||||
this.alignment = alignment;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public ColumnBuilder<T> string(Function<T,String> getValueFn) {
|
||||
Objects.requireNonNull(getValueFn);
|
||||
this.columnDataType = ColumnDataType.STRING;
|
||||
this.getValueFn = getValueFn;
|
||||
this.formatFn = s -> String.format("%s", s);
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> string(Function<T,String> getValueFn, BiConsumer<T,String> setValueFn) {
|
||||
Objects.requireNonNull(getValueFn);
|
||||
Objects.requireNonNull(setValueFn);
|
||||
this.setValueFn = (e,t) -> setValueFn.accept(builder.fileEntryType.cast(e), t);
|
||||
return string(getValueFn);
|
||||
}
|
||||
public ColumnBuilder<T> integer(Function<T,Number> getValueFn, String fmt) {
|
||||
Objects.requireNonNull(getValueFn);
|
||||
Objects.requireNonNull(fmt);
|
||||
this.columnDataType = ColumnDataType.INTEGER;
|
||||
this.getValueFn = getValueFn;
|
||||
this.formatFn = n -> String.format(fmt, n);
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> integer(Function<T,Number> getValueFn, String fmt, BiConsumer<T,Integer> setValueFn) {
|
||||
Objects.requireNonNull(getValueFn);
|
||||
Objects.requireNonNull(fmt);
|
||||
Objects.requireNonNull(setValueFn);
|
||||
this.setValueFn = (e,t) -> setInteger(e, t, setValueFn);
|
||||
return integer(getValueFn, fmt);
|
||||
}
|
||||
public ColumnBuilder<T> real(Function<T,Number> getValueFn, String fmt) {
|
||||
Objects.requireNonNull(getValueFn);
|
||||
Objects.requireNonNull(fmt);
|
||||
this.columnDataType = ColumnDataType.REAL;
|
||||
this.getValueFn = getValueFn;
|
||||
this.formatFn = n -> String.format(fmt, n);
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> date(Function<T,Date> getValueFn) {
|
||||
Objects.requireNonNull(getValueFn);
|
||||
this.columnDataType = ColumnDataType.STRING;
|
||||
this.getValueFn = getValueFn;
|
||||
this.formatFn = this::formatDate;
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> fileTime(Function<T, FileTime> getValueFn) {
|
||||
Objects.requireNonNull(getValueFn);
|
||||
this.columnDataType = ColumnDataType.STRING;
|
||||
this.getValueFn = getValueFn;
|
||||
this.formatFn = this::formatDate;
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> percent(Function<T, Number> numeratorFn, Function<T, Number> denominatorFn, String fmt) {
|
||||
Objects.requireNonNull(numeratorFn);
|
||||
Objects.requireNonNull(denominatorFn);
|
||||
Objects.requireNonNull(fmt);
|
||||
this.columnDataType = ColumnDataType.REAL;
|
||||
this.getValueFn = t -> percentage(t, numeratorFn, denominatorFn);
|
||||
this.formatFn = d -> String.format(fmt, d);
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> mode(DisplayMode... modes) {
|
||||
this.modes = modes;
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> editInLine() {
|
||||
this.editInLine = true;
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> maxLength(int maxLength) {
|
||||
this.maxLength = maxLength;
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> option(String value, String description) {
|
||||
Objects.requireNonNull(value);
|
||||
Objects.requireNonNull(description);
|
||||
this.options.add(new ColumnOption(value, description));
|
||||
return this;
|
||||
}
|
||||
public ColumnBuilder<T> restrictToHexValues() {
|
||||
this.validCharacters = CharacterMapper.builder()
|
||||
.mapRange('a', 'f', false)
|
||||
.mapRange('A', 'F', false)
|
||||
.mapRange('0', '9', false)
|
||||
.map('$', '$') // $1234 format
|
||||
.map('x', 'x') // 0x1234 format
|
||||
.map('X', 'X') // 0X1234 format
|
||||
.map('h', 'h') // 1234h format
|
||||
.map('H', 'H') // 1234H format
|
||||
.build();
|
||||
this.validInputPattern = VALID_HEX_NUMBER;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> done() {
|
||||
// A helper that allows a short-cut of 0 modes meaning ALL modes.
|
||||
if (modes == null || modes.length == 0) {
|
||||
modes = new DisplayMode[] { DisplayMode.NATIVE, DisplayMode.DETAIL };
|
||||
}
|
||||
// Give a default map everything character mapper
|
||||
if (validCharacters == null) {
|
||||
validCharacters = CharacterMapper.builder().build();
|
||||
}
|
||||
// This is where generics get sticky. Either the entirety of the API is fully generic, making a bunch
|
||||
// of awkward usages with FileStore<?> etc. Or we do magic stuff like this to keep is simple.
|
||||
Function<FileEntry, Object> convertedFn = f -> getValueFn.apply(builder.fileEntryType.cast(f));
|
||||
builder.columns.add(new DisplayColumn(title, alignment, Set.of(modes), columnDataType, convertedFn, formatFn,
|
||||
setValueFn, editInLine, maxLength, options,
|
||||
validCharacters, validInputPattern));
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// This is a helper function to format date objects.
|
||||
private String formatDate(Object value) {
|
||||
if (value == null) {
|
||||
return "- No Date -";
|
||||
}
|
||||
return switch (value) {
|
||||
case Date date -> {
|
||||
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
yield localDateTime.format(DATE_TIME_FORMATTER);
|
||||
}
|
||||
case FileTime fileTime -> {
|
||||
LocalDateTime localDateTime = fileTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
yield localDateTime.format(DATE_TIME_FORMATTER);
|
||||
|
||||
}
|
||||
default -> "(Unknown Date)";
|
||||
};
|
||||
}
|
||||
|
||||
/// This is a helper function to help compute percentages.
|
||||
private double percentage(FileEntry fileEntry, Function<T, Number> numeratorFn, Function<T, Number> denominatorFn) {
|
||||
T typedFileEntry = builder.fileEntryType.cast(fileEntry);
|
||||
Number numerator = numeratorFn.apply(typedFileEntry);
|
||||
Number denominator = denominatorFn.apply(typedFileEntry);
|
||||
if (denominator == null || denominator.doubleValue() == 0) return 0;
|
||||
return (1.0 - numerator.doubleValue() / denominator.doubleValue()) * 100.0;
|
||||
}
|
||||
|
||||
private void setInteger(FileEntry fileEntry, String text, BiConsumer<T,Integer> setter) {
|
||||
T typedFileEntry = builder.fileEntryType.cast(fileEntry);
|
||||
int value = 0;
|
||||
if (text.startsWith("0x") || text.startsWith("0X")) {
|
||||
value = Integer.parseInt(text.substring(2), 16);
|
||||
}
|
||||
else if (text.startsWith("$")) {
|
||||
value = Integer.parseInt(text.substring(1), 16);
|
||||
}
|
||||
else if (text.endsWith("h") || text.endsWith("H")) {
|
||||
value = Integer.parseInt(text.substring(0, text.length()-1), 16);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// We may not be able to distinguish base 10 from base 16. Try the more restrictive one first!
|
||||
value = Integer.parseInt(text);
|
||||
} catch (NumberFormatException e) {
|
||||
value = Integer.parseInt(text, 16);
|
||||
}
|
||||
}
|
||||
setter.accept(typedFileEntry, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.ui;
|
||||
|
||||
/// Indicates the display mode to help filter columns to users' preference.
|
||||
public enum DisplayMode {
|
||||
NATIVE,
|
||||
DETAIL
|
||||
}
|
||||
@@ -21,6 +21,8 @@ package org.applecommander.filestore;
|
||||
|
||||
import org.applecommander.capability.Capability;
|
||||
import org.applecommander.source.Source;
|
||||
import org.applecommander.ui.DisplayColumn;
|
||||
import org.applecommander.ui.DisplayMode;
|
||||
import org.applecommander.usage.BlockUsage;
|
||||
import org.applecommander.usage.DiskUsage;
|
||||
import org.applecommander.usage.DiskUsage.UsageType;
|
||||
@@ -57,13 +59,13 @@ public abstract class FileStoreTestHelper {
|
||||
public static void showFiles(Directory parent, String indent) {
|
||||
FileStore fileStore = parent.getFileStore();
|
||||
List<DisplayColumn> displayColumns = fileStore.getDisplayColumns().stream()
|
||||
.filter(entry -> entry.supports(DisplayColumn.Mode.DETAIL))
|
||||
.filter(entry -> entry.supports(DisplayMode.DETAIL))
|
||||
.toList();
|
||||
if (parent.getParent().isEmpty()) {
|
||||
// Print columns only at the top
|
||||
System.out.print(indent);
|
||||
displayColumns.forEach(column -> {
|
||||
System.out.print(column.headerText());
|
||||
System.out.print(column.getTitle());
|
||||
System.out.print(" ");
|
||||
});
|
||||
System.out.println();
|
||||
@@ -72,7 +74,7 @@ public abstract class FileStoreTestHelper {
|
||||
if (!file.isDeleted()) {
|
||||
System.out.print(indent);
|
||||
displayColumns.forEach(column -> {
|
||||
System.out.print(column.formatAsText(file));
|
||||
System.out.print(column.formatObject(file));
|
||||
System.out.print(" ");
|
||||
});
|
||||
System.out.println();
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.ui;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class CharacterMapperTest {
|
||||
@Test
|
||||
public void testForcedUppercase() {
|
||||
CharacterMapper hex = CharacterMapper.builder()
|
||||
.mapRange('a', 'f', true)
|
||||
.mapRange('A', 'F', false)
|
||||
.mapRange('0', '9', false)
|
||||
.map('$', '$')
|
||||
.build();
|
||||
|
||||
assertEquals("$12AF", hex.map(">>> $12af <<<"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMixedCase() {
|
||||
CharacterMapper hex = CharacterMapper.builder()
|
||||
.mapRange('a', 'f', false)
|
||||
.mapRange('A', 'F', false)
|
||||
.mapRange('0', '9', false)
|
||||
.map('$', '$')
|
||||
.build();
|
||||
|
||||
assertEquals("$12af", hex.map(">>> $12af <<<"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.ui;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class DisplayColumnTest {
|
||||
@Test
|
||||
public void testHexPattern() {
|
||||
Pattern pattern = DisplayColumn.ColumnBuilder.VALID_HEX_NUMBER;
|
||||
assertNotNull(pattern);
|
||||
assertTrue(pattern.matcher("$12ab").matches());
|
||||
assertTrue(pattern.matcher("0x12AB").matches());
|
||||
assertTrue(pattern.matcher("12AbH").matches());
|
||||
assertFalse(pattern.matcher("he110").matches());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user