From 6341a7fa4254750399f4eb49754673dbd37052c4 Mon Sep 17 00:00:00 2001 From: a2geek Date: Thu, 15 Oct 2026 17:15:17 -0500 Subject: [PATCH 01/11] Moved and redesigned DisplayColumn. Still need to design the editable column, but this is a little bit simpler. --- .../org/applecommander/javafx/FileView.java | 97 +++++----- .../storage/DiskFileStoreAdapter.java | 38 ++-- lib/ac-api/src/main/java/module-info.java | 1 + .../aaf/AppleArchiveFormatFileStore.java | 10 +- .../archive/shrinkit/ShrinkitFileStore.java | 39 ++-- .../archive/zip/ZipFileStore.java | 22 +-- .../filestore/DisplayColumn.java | 174 ------------------ .../applecommander/filestore/FileStore.java | 1 + .../os/gamedos/GamedosFileStore.java | 19 +- .../applecommander/ui/ColumnAlignment.java | 8 + .../org/applecommander/ui/ColumnDataType.java | 10 + .../org/applecommander/ui/DisplayColumn.java | 161 ++++++++++++++++ .../org/applecommander/ui/DisplayMode.java | 7 + .../filestore/FileStoreTestHelper.java | 8 +- 14 files changed, 307 insertions(+), 288 deletions(-) delete mode 100644 lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java create mode 100644 lib/ac-api/src/main/java/org/applecommander/ui/ColumnAlignment.java create mode 100644 lib/ac-api/src/main/java/org/applecommander/ui/ColumnDataType.java create mode 100644 lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java create mode 100644 lib/ac-api/src/main/java/org/applecommander/ui/DisplayMode.java 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 62b96289..1d13028c 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 @@ -28,7 +28,6 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; 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.HBox; @@ -50,6 +49,9 @@ 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.DisplayColumn; +import org.applecommander.ui.DisplayMode; import java.io.ByteArrayOutputStream; import java.io.File; @@ -83,7 +85,7 @@ public class FileView extends BorderPane { private final ObservableList directoryPath = FXCollections.observableArrayList(); private final ObjectProperty selectedDirectory = new SimpleObjectProperty<>(); - private final ObjectProperty listingMode = new SimpleObjectProperty<>(DisplayColumn.Mode.NATIVE); + private final ObjectProperty listingMode = new SimpleObjectProperty<>(DisplayMode.NATIVE); private final SimpleBooleanProperty supportsDirectories = new SimpleBooleanProperty(false); private final SimpleBooleanProperty supportsFileDeletion = new SimpleBooleanProperty(false); private final ObservableList fileEntries = FXCollections.observableArrayList(); @@ -125,7 +127,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 +136,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)); @@ -383,7 +385,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 +394,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 +418,30 @@ public class FileView extends BorderPane { boolean editable = false; List> tableColumns = new ArrayList<>(); for (final DisplayColumn displayColumn : displayColumns) { - TableColumn column = switch (displayColumn.dataType()) { + TableColumn column = switch (displayColumn.getDataType()) { 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.addChange(); - }); - return property; - }); - editable = true; - } else { + TableColumn stringColumn = new TableColumn<>(displayColumn.getTitle()); +// TODO +// if (displayColumn.editInline()) { +// stringColumn.setCellFactory(TextFieldTableCell.forTableColumn()); +// stringColumn.setEditable(true); +// stringColumn.setCellValueFactory(cell -> { +// SimpleStringProperty property = new SimpleStringProperty(displayColumn.getDisplayText(cell.getValue())); +// property.addListener((_, _, newValue) -> { +// displayColumn.setValueFn().accept(cell.getValue(), newValue); +// fileStoreWindow.addChange(); +// }); +// return property; +// }); +// editable = true; +// } else { stringColumn.setCellValueFactory(cell -> - new SimpleStringProperty(displayColumn.formatAsText(cell.getValue()))); - } + new SimpleStringProperty(displayColumn.getDisplayText(cell.getValue()))); +// } yield stringColumn; } - case INTEGER,LONG,DOUBLE -> { - TableColumn numberColumn = new TableColumn<>(displayColumn.headerText()); + case INTEGER,REAL -> { + TableColumn numberColumn = new TableColumn<>(displayColumn.getTitle()); numberColumn.setCellFactory(c -> new TableCell<>() { @Override protected void updateItem(Number item, boolean empty) { @@ -446,35 +449,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); } @@ -510,8 +509,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() { 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 a353ce07..87714292 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 @@ -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,31 @@ public class DiskFileStoreAdapter implements FileStore { /// old API pre-formats everything as a String. @Override public List getDisplayColumns() { - List columns = new ArrayList<>(); + DisplayColumn.Builder builder = DisplayColumn.builder(DiskFileEntryAdapter.class); for (int displayMode : List.of(FormattedDisk.FILE_DISPLAY_NATIVE, FormattedDisk.FILE_DISPLAY_DETAIL)) { List 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 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 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()); - }; - DisplayColumn displayColumn = new DisplayColumn(header.getTitle(), - alignment, DisplayColumn.DataType.STRING, mappingFn::apply, null, false, "%s", mode); - columns.add(displayColumn); + Function getValueFn = entry -> + entry.fileEntry.getFileColumnData(displayMode).get(headerIndex); + switch (header.getAlignment()) { + case FormattedDisk.FileColumnHeader.ALIGN_CENTER -> builder.centerString(title, getValueFn, mode); + case FormattedDisk.FileColumnHeader.ALIGN_RIGHT -> builder.rightString(title, getValueFn, mode); + default -> builder.leftString(title, getValueFn, mode); + } } } - return columns; + return builder.toList(); } @Override diff --git a/lib/ac-api/src/main/java/module-info.java b/lib/ac-api/src/main/java/module-info.java index 0095632c..00590f0f 100644 --- a/lib/ac-api/src/main/java/module-info.java +++ b/lib/ac-api/src/main/java/module-info.java @@ -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; diff --git a/lib/ac-api/src/main/java/org/applecommander/archive/aaf/AppleArchiveFormatFileStore.java b/lib/ac-api/src/main/java/org/applecommander/archive/aaf/AppleArchiveFormatFileStore.java index f78b958a..85682468 100644 --- a/lib/ac-api/src/main/java/org/applecommander/archive/aaf/AppleArchiveFormatFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/archive/aaf/AppleArchiveFormatFileStore.java @@ -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 getDisplayColumns() { return DisplayColumn.builder(AppleArchiveFormatFileEntry.class) - .addStringField("Name", AppleArchiveFormatFileEntry::getName) - .addLongField("Size", AppleArchiveFormatFileEntry::getSize) + .leftString("Name", AppleArchiveFormatFileEntry::getName) + .rightNumber("Size", AppleArchiveFormatFileEntry::getSize, "%,d") .toList(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/archive/shrinkit/ShrinkitFileStore.java b/lib/ac-api/src/main/java/org/applecommander/archive/shrinkit/ShrinkitFileStore.java index b8adedbe..9eef0644 100644 --- a/lib/ac-api/src/main/java/org/applecommander/archive/shrinkit/ShrinkitFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/archive/shrinkit/ShrinkitFileStore.java @@ -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 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) + .leftString("Name", ShrinkitFileEntry::getName) + .leftString("Sys. Id", ShrinkitFileEntry::getFileSysIdText, DisplayMode.DETAIL) + .rightNumber("File Sys. Info", ShrinkitFileEntry::getFileSysInfo, "$%04X", DisplayMode.DETAIL) + .centerString("Access", ShrinkitFileEntry::getAccessText, DisplayMode.DETAIL) + .leftString("Kind", ShrinkitFileEntry::getStorageTypeText) + .centerString("Type", ShrinkitFileEntry::getFileTypeText) + .rightNumber("Aux. Type", ShrinkitFileEntry::getExtraType, "$%04X") + .centerDate("Archived", ShrinkitFileEntry::getArchiveWhen) + .centerDate("Created", ShrinkitFileEntry::getCreateWhen, DisplayMode.DETAIL) + .centerDate("Modified", ShrinkitFileEntry::getModWhen, DisplayMode.DETAIL) + .leftString("Format", ShrinkitFileEntry::getThreadFormatText) + .rightPercent("Size", ShrinkitFileEntry::getCompressedSize, ShrinkitFileEntry::getSize, "%2.0f%%") + .rightNumber("Un-Length", ShrinkitFileEntry::getSize, "%,d") + .rightNumber("Data CRC", ShrinkitFileEntry::getDataForkCrc, "$%04X", DisplayMode.DETAIL) + .rightNumber("Resource CRC", ShrinkitFileEntry::getResourceForkCrc, "$%04X", DisplayMode.DETAIL) .toList(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java b/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java index 36243c57..f66d0a65 100644 --- a/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java @@ -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; @@ -91,15 +91,15 @@ public class ZipFileStore implements FileStore { @Override public List 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) + .rightNumber("Length", FileEntry::getSize, "%,d") + .leftString("Method", ZipFileEntry::getMethodName, DisplayMode.DETAIL) + .rightNumber("Size", ZipFileEntry::getCompressedSize, "%,d", DisplayMode.DETAIL) + .rightPercent("Ratio", ZipFileEntry::getCompressedSize, ZipFileEntry::getSize, "%2.0f%%", DisplayMode.DETAIL) + .centerFileTime("Creation", ZipFileEntry::getCreationTime) + .centerFileTime("Last Access", ZipFileEntry::getLastAccessTime, DisplayMode.DETAIL) + .centerFileTime("Last Modified", ZipFileEntry::getLastModifiedTime) + .rightNumber("CRC-32", ZipFileEntry::getCrc, "%08x", DisplayMode.DETAIL) + .leftString("Name", FileEntry::getName) .toList(); } 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 deleted file mode 100644 index 79e3bd0c..00000000 --- a/lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java +++ /dev/null @@ -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 getValueFn, BiConsumer 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 Builder builder(Class clazz) { - return new Builder<>(clazz); - } - public static class Builder { - private static final DateTimeFormatter FILE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"); - private final Class clazz; - private final List columns = new ArrayList<>(); - - private Builder(Class clazz) { - this.clazz = clazz; - } - - public List toList() { - return columns; - } - - /// A helper method that allows a short-cut of 0 modes meaning ALL modes. - 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, dataType, getValueFn, setValueFn, editInline, fmt, modes)); - return this; - } - private Builder add(String name, Alignment alignment, DataType dataType, Function getValueFn, - String fmt, Mode... 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, 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, 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, DataType.STRING, entry -> convert(entry, valueFn), "%s", modes); - } - public Builder addStringField(String name, Alignment alignment, Function valueFn, Mode ...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, DataType.DOUBLE, entry -> percentage(entry, numeratorFn, denominatorFn), fmt, modes); - } - public Builder addFileTimeField(String name, Function valueFn, Mode ...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, DataType.LONG, entry -> convert(entry, valueFn), fmt, modes); - } - public Builder addDateField(String name, Function valueFn, Mode ...modes) { - return add(name, Alignment.CENTER, DataType.STRING, entry -> formatDate(entry, valueFn), "%s", modes); - } - - private S convert(FileEntry fileEntry, Function valueFn) { - T typedFileEntry = clazz.cast(fileEntry); - return valueFn.apply(typedFileEntry); - } - private void convert(FileEntry fileEntry, BiConsumer setValueFn, Class valueClass, Object value) { - T typedFileEntry = clazz.cast(fileEntry); - S typedValue = valueClass.cast(value); - setValueFn.accept(typedFileEntry, typedValue); - } - private double percentage(FileEntry fileEntry, Function numeratorFn, Function 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 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 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); - } - } -} \ No newline at end of file diff --git a/lib/ac-api/src/main/java/org/applecommander/filestore/FileStore.java b/lib/ac-api/src/main/java/org/applecommander/filestore/FileStore.java index affaad1a..09dd25bc 100644 --- a/lib/ac-api/src/main/java/org/applecommander/filestore/FileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/filestore/FileStore.java @@ -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; diff --git a/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java b/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java index 7d2ae9d7..5298fad4 100644 --- a/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java @@ -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,13 @@ public class GamedosFileStore implements FileStore, GamedosConstants { @Override public List 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) + .leftString("Type", GamedosFileEntry::getFiletype) + .leftString("Name", GamedosFileEntry::getName) + .rightNumber("Meta", GamedosFileEntry::getMeta, "$%04X") + .rightNumber("First Track", GamedosFileEntry::getFirstTrack, "%d", DisplayMode.DETAIL) + .rightNumber("First Sector", GamedosFileEntry::getFirstSector, "%d", DisplayMode.DETAIL) + .rightNumber("Sector Count", GamedosFileEntry::getSectorCount, "%d", DisplayMode.DETAIL) + .rightNumber("Size", GamedosFileEntry::getSize, "%,d") .toList(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnAlignment.java b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnAlignment.java new file mode 100644 index 00000000..3f2b9e27 --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnAlignment.java @@ -0,0 +1,8 @@ +package org.applecommander.ui; + +/// Indicates how this column should be aligned. +public enum ColumnAlignment { + LEFT, + CENTER, + RIGHT +} diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnDataType.java b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnDataType.java new file mode 100644 index 00000000..f756bc4f --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnDataType.java @@ -0,0 +1,10 @@ +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 +} diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java new file mode 100644 index 00000000..d41d762e --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java @@ -0,0 +1,161 @@ +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.Function; + +/// A DisplayColumn supports formatted output from various FileStores in a +/// dynamic fashion. The intent is to provide a flexible display interface +/// across interfaces that does not rely on hard-coded implementations. +public class DisplayColumn { + private final String title; + private final ColumnAlignment alignment; + private final Set modes; + private final ColumnDataType columnDataType; + private final Function getValueFn; + private final Function formatFn; + + private DisplayColumn(String title, ColumnAlignment alignment, Set modes, ColumnDataType columnDataType, + Function getValueFn, + Function formatFn) { + Objects.requireNonNull(title); + Objects.requireNonNull(alignment); + Objects.requireNonNull(modes); + Objects.requireNonNull(columnDataType); + Objects.requireNonNull(getValueFn); + Objects.requireNonNull(formatFn); + this.title = title; + this.alignment = alignment; + this.modes = modes; + this.columnDataType = columnDataType; + this.getValueFn = getValueFn; + this.formatFn = formatFn; + } + + /// 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 static Builder builder(Class fileEntryType) { + return new Builder<>(fileEntryType); + } + + public static class Builder { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"); + + private final Class fileEntryType; + private final List columns = new ArrayList<>(); + + private Builder(Class fileEntryType) { + this.fileEntryType = fileEntryType; + } + + /// A helper method that allows a short-cut of 0 modes meaning ALL modes. + private Builder add(String name, ColumnAlignment alignment, ColumnDataType columnDataType, + Function getValueFn, Function formatFn, DisplayMode... modes) { + if (modes.length == 0) { + modes = new DisplayMode[] { DisplayMode.NATIVE, DisplayMode.DETAIL }; + } + // 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 convertedFn = f -> getValueFn.apply(fileEntryType.cast(f)); + columns.add(new DisplayColumn(name, alignment, Set.of(modes), columnDataType, convertedFn, formatFn)); + return this; + } + + /// Add a left-aligned string field. + public Builder leftString(String name, Function getValueFn, DisplayMode... modes) { + return add(name, ColumnAlignment.LEFT, ColumnDataType.STRING, getValueFn, s -> String.format("%s",s), modes); + } + /// Add a centered string field. + public Builder centerString(String name, Function getValueFn, DisplayMode... modes) { + return add(name, ColumnAlignment.CENTER, ColumnDataType.STRING, getValueFn, s -> String.format("%s",s), modes); + } + /// Add a right-aligned string field. + public Builder rightString(String name, Function getValueFn, DisplayMode... modes) { + return add(name, ColumnAlignment.RIGHT, ColumnDataType.STRING, getValueFn, s -> String.format("%s",s), modes); + } + /// Add a right-aligned integer field. Mathematical definition of this includes "whole numbers and their negatives". + /// It covers `byte`, `short`, `int`, `long` and their respective objects. + public Builder rightNumber(String name, Function getValueFn, String fmt, DisplayMode... modes) { + return add(name, ColumnAlignment.RIGHT, ColumnDataType.INTEGER, getValueFn, n -> String.format(fmt,n), modes); + } + /// Add a centered Date field. + public Builder centerDate(String name, Function getValueFn, DisplayMode... modes) { + return add(name, ColumnAlignment.CENTER, ColumnDataType.STRING, getValueFn, this::formatDate, modes); + } + /// Add a centered FileTime field. + public Builder centerFileTime(String name, Function getValueFn, DisplayMode... modes) { + return add(name, ColumnAlignment.CENTER, ColumnDataType.STRING, getValueFn, this::formatDate, modes); + } + /// Add a right-aligned percent field. + public Builder rightPercent(String name, Function numeratorFn, Function denominatorFn, + String fmt, DisplayMode... modes) { + return add(name, ColumnAlignment.RIGHT, ColumnDataType.REAL, + t -> percentage(t,numeratorFn,denominatorFn), + d -> String.format(fmt, d), modes); + } + + public List toList() { + return columns; + } + + /// 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 numeratorFn, Function denominatorFn) { + T typedFileEntry = 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; + } + } +} diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayMode.java b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayMode.java new file mode 100644 index 00000000..0cb85194 --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayMode.java @@ -0,0 +1,7 @@ +package org.applecommander.ui; + +/// Indicates the display mode to help filter columns to users' preference. +public enum DisplayMode { + NATIVE, + DETAIL +} diff --git a/lib/ac-api/src/test/java/org/applecommander/filestore/FileStoreTestHelper.java b/lib/ac-api/src/test/java/org/applecommander/filestore/FileStoreTestHelper.java index f775ec53..f92abbd6 100644 --- a/lib/ac-api/src/test/java/org/applecommander/filestore/FileStoreTestHelper.java +++ b/lib/ac-api/src/test/java/org/applecommander/filestore/FileStoreTestHelper.java @@ -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 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(); From 57b8817b4024dd3e52a2e961633363168e8752b1 Mon Sep 17 00:00:00 2001 From: a2geek Date: Thu, 15 Oct 2026 19:55:32 -0500 Subject: [PATCH 02/11] Revised builder in preparation to start enabling editable attributes. --- .../storage/DiskFileStoreAdapter.java | 11 +- .../aaf/AppleArchiveFormatFileStore.java | 4 +- .../archive/shrinkit/ShrinkitFileStore.java | 30 ++-- .../archive/zip/ZipFileStore.java | 21 +-- .../os/gamedos/GamedosFileStore.java | 14 +- .../org/applecommander/ui/ColumnOption.java | 4 + .../org/applecommander/ui/DisplayColumn.java | 148 ++++++++++++------ 7 files changed, 144 insertions(+), 88 deletions(-) create mode 100644 lib/ac-api/src/main/java/org/applecommander/ui/ColumnOption.java 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 87714292..c1d90d73 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 @@ -164,11 +164,12 @@ public class DiskFileStoreAdapter implements FileStore { final int headerIndex = i; Function getValueFn = entry -> entry.fileEntry.getFileColumnData(displayMode).get(headerIndex); - switch (header.getAlignment()) { - case FormattedDisk.FileColumnHeader.ALIGN_CENTER -> builder.centerString(title, getValueFn, mode); - case FormattedDisk.FileColumnHeader.ALIGN_RIGHT -> builder.rightString(title, getValueFn, mode); - default -> builder.leftString(title, getValueFn, mode); - } + DisplayColumn.ColumnBuilder column = switch (header.getAlignment()) { + case FormattedDisk.FileColumnHeader.ALIGN_CENTER -> builder.center(title); + case FormattedDisk.FileColumnHeader.ALIGN_RIGHT -> builder.right(title); + default -> builder.left(title); + }; + column.string(getValueFn).mode(mode).done(); } } return builder.toList(); diff --git a/lib/ac-api/src/main/java/org/applecommander/archive/aaf/AppleArchiveFormatFileStore.java b/lib/ac-api/src/main/java/org/applecommander/archive/aaf/AppleArchiveFormatFileStore.java index 85682468..80aeef03 100644 --- a/lib/ac-api/src/main/java/org/applecommander/archive/aaf/AppleArchiveFormatFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/archive/aaf/AppleArchiveFormatFileStore.java @@ -98,8 +98,8 @@ public class AppleArchiveFormatFileStore implements FileStore { @Override public List getDisplayColumns() { return DisplayColumn.builder(AppleArchiveFormatFileEntry.class) - .leftString("Name", AppleArchiveFormatFileEntry::getName) - .rightNumber("Size", AppleArchiveFormatFileEntry::getSize, "%,d") + .left("Name").string(AppleArchiveFormatFileEntry::getName).done() + .right("Size").integer(AppleArchiveFormatFileEntry::getSize, "%,d").done() .toList(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/archive/shrinkit/ShrinkitFileStore.java b/lib/ac-api/src/main/java/org/applecommander/archive/shrinkit/ShrinkitFileStore.java index 9eef0644..13c05d59 100644 --- a/lib/ac-api/src/main/java/org/applecommander/archive/shrinkit/ShrinkitFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/archive/shrinkit/ShrinkitFileStore.java @@ -81,21 +81,21 @@ public class ShrinkitFileStore implements FileStore { @Override public List getDisplayColumns() { return DisplayColumn.builder(ShrinkitFileEntry.class) - .leftString("Name", ShrinkitFileEntry::getName) - .leftString("Sys. Id", ShrinkitFileEntry::getFileSysIdText, DisplayMode.DETAIL) - .rightNumber("File Sys. Info", ShrinkitFileEntry::getFileSysInfo, "$%04X", DisplayMode.DETAIL) - .centerString("Access", ShrinkitFileEntry::getAccessText, DisplayMode.DETAIL) - .leftString("Kind", ShrinkitFileEntry::getStorageTypeText) - .centerString("Type", ShrinkitFileEntry::getFileTypeText) - .rightNumber("Aux. Type", ShrinkitFileEntry::getExtraType, "$%04X") - .centerDate("Archived", ShrinkitFileEntry::getArchiveWhen) - .centerDate("Created", ShrinkitFileEntry::getCreateWhen, DisplayMode.DETAIL) - .centerDate("Modified", ShrinkitFileEntry::getModWhen, DisplayMode.DETAIL) - .leftString("Format", ShrinkitFileEntry::getThreadFormatText) - .rightPercent("Size", ShrinkitFileEntry::getCompressedSize, ShrinkitFileEntry::getSize, "%2.0f%%") - .rightNumber("Un-Length", ShrinkitFileEntry::getSize, "%,d") - .rightNumber("Data CRC", ShrinkitFileEntry::getDataForkCrc, "$%04X", DisplayMode.DETAIL) - .rightNumber("Resource CRC", ShrinkitFileEntry::getResourceForkCrc, "$%04X", DisplayMode.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(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java b/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java index f66d0a65..c3727b95 100644 --- a/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java @@ -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 getDisplayColumns() { - return DisplayColumn.builder(ZipFileEntry.class) - .rightNumber("Length", FileEntry::getSize, "%,d") - .leftString("Method", ZipFileEntry::getMethodName, DisplayMode.DETAIL) - .rightNumber("Size", ZipFileEntry::getCompressedSize, "%,d", DisplayMode.DETAIL) - .rightPercent("Ratio", ZipFileEntry::getCompressedSize, ZipFileEntry::getSize, "%2.0f%%", DisplayMode.DETAIL) - .centerFileTime("Creation", ZipFileEntry::getCreationTime) - .centerFileTime("Last Access", ZipFileEntry::getLastAccessTime, DisplayMode.DETAIL) - .centerFileTime("Last Modified", ZipFileEntry::getLastModifiedTime) - .rightNumber("CRC-32", ZipFileEntry::getCrc, "%08x", DisplayMode.DETAIL) - .leftString("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).done() .toList(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java b/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java index 5298fad4..1b0ba177 100644 --- a/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java @@ -107,13 +107,13 @@ public class GamedosFileStore implements FileStore, GamedosConstants { @Override public List getDisplayColumns() { return DisplayColumn.builder(GamedosFileEntry.class) - .leftString("Type", GamedosFileEntry::getFiletype) - .leftString("Name", GamedosFileEntry::getName) - .rightNumber("Meta", GamedosFileEntry::getMeta, "$%04X") - .rightNumber("First Track", GamedosFileEntry::getFirstTrack, "%d", DisplayMode.DETAIL) - .rightNumber("First Sector", GamedosFileEntry::getFirstSector, "%d", DisplayMode.DETAIL) - .rightNumber("Sector Count", GamedosFileEntry::getSectorCount, "%d", DisplayMode.DETAIL) - .rightNumber("Size", GamedosFileEntry::getSize, "%,d") + .left("Type").string(GamedosFileEntry::getFiletype).done() + .left("Name").string(GamedosFileEntry::getName).done() + .right("Meta").integer(GamedosFileEntry::getMeta, "$%04X").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(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnOption.java b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnOption.java new file mode 100644 index 00000000..12c53e42 --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnOption.java @@ -0,0 +1,4 @@ +package org.applecommander.ui; + +public record ColumnOption(String value, String description) { +} diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java index d41d762e..09e5a463 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java @@ -20,9 +20,8 @@ public class DisplayColumn { private final Function getValueFn; private final Function formatFn; - private DisplayColumn(String title, ColumnAlignment alignment, Set modes, ColumnDataType columnDataType, - Function getValueFn, - Function formatFn) { + protected DisplayColumn(String title, ColumnAlignment alignment, Set modes, ColumnDataType columnDataType, + Function getValueFn, Function formatFn) { Objects.requireNonNull(title); Objects.requireNonNull(alignment); Objects.requireNonNull(modes); @@ -72,64 +71,114 @@ public class DisplayColumn { } public static class Builder { - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"); - private final Class fileEntryType; private final List columns = new ArrayList<>(); private Builder(Class fileEntryType) { + Objects.requireNonNull(fileEntryType); this.fileEntryType = fileEntryType; } - /// A helper method that allows a short-cut of 0 modes meaning ALL modes. - private Builder add(String name, ColumnAlignment alignment, ColumnDataType columnDataType, - Function getValueFn, Function formatFn, DisplayMode... modes) { - if (modes.length == 0) { - modes = new DisplayMode[] { DisplayMode.NATIVE, DisplayMode.DETAIL }; - } - // 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 convertedFn = f -> getValueFn.apply(fileEntryType.cast(f)); - columns.add(new DisplayColumn(name, alignment, Set.of(modes), columnDataType, convertedFn, formatFn)); - return this; + public ColumnBuilder left(String title) { + Objects.requireNonNull(title); + return new ColumnBuilder<>(this, ColumnAlignment.LEFT, title); } - - /// Add a left-aligned string field. - public Builder leftString(String name, Function getValueFn, DisplayMode... modes) { - return add(name, ColumnAlignment.LEFT, ColumnDataType.STRING, getValueFn, s -> String.format("%s",s), modes); + public ColumnBuilder center(String title) { + Objects.requireNonNull(title); + return new ColumnBuilder<>(this, ColumnAlignment.CENTER, title); } - /// Add a centered string field. - public Builder centerString(String name, Function getValueFn, DisplayMode... modes) { - return add(name, ColumnAlignment.CENTER, ColumnDataType.STRING, getValueFn, s -> String.format("%s",s), modes); - } - /// Add a right-aligned string field. - public Builder rightString(String name, Function getValueFn, DisplayMode... modes) { - return add(name, ColumnAlignment.RIGHT, ColumnDataType.STRING, getValueFn, s -> String.format("%s",s), modes); - } - /// Add a right-aligned integer field. Mathematical definition of this includes "whole numbers and their negatives". - /// It covers `byte`, `short`, `int`, `long` and their respective objects. - public Builder rightNumber(String name, Function getValueFn, String fmt, DisplayMode... modes) { - return add(name, ColumnAlignment.RIGHT, ColumnDataType.INTEGER, getValueFn, n -> String.format(fmt,n), modes); - } - /// Add a centered Date field. - public Builder centerDate(String name, Function getValueFn, DisplayMode... modes) { - return add(name, ColumnAlignment.CENTER, ColumnDataType.STRING, getValueFn, this::formatDate, modes); - } - /// Add a centered FileTime field. - public Builder centerFileTime(String name, Function getValueFn, DisplayMode... modes) { - return add(name, ColumnAlignment.CENTER, ColumnDataType.STRING, getValueFn, this::formatDate, modes); - } - /// Add a right-aligned percent field. - public Builder rightPercent(String name, Function numeratorFn, Function denominatorFn, - String fmt, DisplayMode... modes) { - return add(name, ColumnAlignment.RIGHT, ColumnDataType.REAL, - t -> percentage(t,numeratorFn,denominatorFn), - d -> String.format(fmt, d), modes); + public ColumnBuilder right(String title) { + Objects.requireNonNull(title); + return new ColumnBuilder<>(this, ColumnAlignment.RIGHT, title); } public List toList() { return columns; } + } + + public static class ColumnBuilder { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"); + + private final Builder builder; + private final ColumnAlignment alignment; + private final String title; + private ColumnDataType columnDataType; + private Function getValueFn; + private Function formatFn; + private DisplayMode[] modes; + + private ColumnBuilder(Builder 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 string(Function getValueFn) { + Objects.requireNonNull(getValueFn); + this.columnDataType = ColumnDataType.STRING; + this.getValueFn = getValueFn; + this.formatFn = s -> String.format("%s", s); + return this; + } + public ColumnBuilder integer(Function 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 real(Function 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 date(Function getValueFn) { + Objects.requireNonNull(getValueFn); + this.columnDataType = ColumnDataType.STRING; + this.getValueFn = getValueFn; + this.formatFn = this::formatDate; + return this; + } + public ColumnBuilder fileTime(Function getValueFn) { + Objects.requireNonNull(getValueFn); + this.columnDataType = ColumnDataType.STRING; + this.getValueFn = getValueFn; + this.formatFn = this::formatDate; + return this; + } + public ColumnBuilder percent(Function numeratorFn, Function 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 mode(DisplayMode... modes) { + this.modes = modes; + return this; + } + + public Builder 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 }; + } + // 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 convertedFn = f -> getValueFn.apply(builder.fileEntryType.cast(f)); + builder.columns.add(new DisplayColumn(title, alignment, Set.of(modes), columnDataType, convertedFn, formatFn)); + return builder; + } /// This is a helper function to format date objects. private String formatDate(Object value) { @@ -149,9 +198,10 @@ public class DisplayColumn { default -> "(Unknown Date)"; }; } + /// This is a helper function to help compute percentages. - private double percentage(FileEntry fileEntry, Function numeratorFn, Function denominatorFn) { - T typedFileEntry = fileEntryType.cast(fileEntry); + private double percentage(FileEntry fileEntry, Function numeratorFn, Function 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; From ab144e4eadd868d17e317661e11357d37c398dd1 Mon Sep 17 00:00:00 2001 From: a2geek Date: Thu, 15 Oct 2026 20:44:33 -0500 Subject: [PATCH 03/11] Added some editable fields. Building out edit components in info pane. --- .../org/applecommander/javafx/FileView.java | 68 +++++++++++++++++++ .../org/applecommander/javafx/FxUtils.java | 18 +++++ .../archive/zip/ZipFileStore.java | 2 +- .../os/gamedos/GamedosFileStore.java | 11 ++- .../org/applecommander/ui/DisplayColumn.java | 44 +++++++++++- 5 files changed, 137 insertions(+), 6 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 1d13028c..2fc5ac03 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 @@ -30,8 +30,10 @@ import javafx.scene.Scene; import javafx.scene.control.*; 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; @@ -50,6 +52,7 @@ 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; @@ -82,6 +85,8 @@ 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 fileTable; + // And the detail grid follows the file table! + private GridPane fileDetailGrid; private final ObservableList directoryPath = FXCollections.observableArrayList(); private final ObjectProperty selectedDirectory = new SimpleObjectProperty<>(); @@ -199,6 +204,26 @@ public class FileView extends BorderPane { fileTable.setPlaceholder(placeholder); setCenter(fileTable); + BooleanProperty showingDetailProperty = new SimpleBooleanProperty(false); + Button detailToggle = FxUtils.createSmallToggleButton("pane-right.png", "pane-left.png", showingDetailProperty); + detailToggle.disableProperty().bind(fileTable.getSelectionModel().selectedItemProperty().isNull()); + fileDetailGrid = new GridPane(); + fileDetailGrid.visibleProperty().bind(showingDetailProperty); + fileDetailGrid.managedProperty().bind(showingDetailProperty); + VBox detailPane = new VBox(detailToggle, fileDetailGrid); + 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); @@ -209,6 +234,49 @@ 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; + 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)); + fileDetailGrid.addRow(row++, label, field); + } + else { + ComboBox combobox = new ComboBox<>(); + 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.setConverter(new StringConverter<>() { + @Override + public String toString(ColumnOption option) { + return String.format("%s - %s", option.value(), option.description()); + } + + @Override + public ColumnOption fromString(String string) { + return null; + } + }); + fileDetailGrid.addRow(row++, label, combobox); + } + } + else { + fileDetailGrid.addRow(row++, label, new Label(displayColumn.getDisplayText(fileEntry))); + } + } + } + private void onDragDroppedHandler(DragEvent event) { Dragboard db = event.getDragboard(); boolean success = false; diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java index 66839407..4c6d3cb6 100644 --- a/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java @@ -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; @@ -85,6 +86,23 @@ 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()); + 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(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java b/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java index c3727b95..fb7930f6 100644 --- a/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/archive/zip/ZipFileStore.java @@ -100,7 +100,7 @@ public class ZipFileStore implements FileStore { .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).done() + .left("Name").string(FileEntry::getName).editInLine().done() .toList(); } diff --git a/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java b/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java index 1b0ba177..d94d0a91 100644 --- a/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java @@ -107,8 +107,15 @@ public class GamedosFileStore implements FileStore, GamedosConstants { @Override public List getDisplayColumns() { return DisplayColumn.builder(GamedosFileEntry.class) - .left("Type").string(GamedosFileEntry::getFiletype).done() - .left("Name").string(GamedosFileEntry::getName).done() + .left("Type").string(GamedosFileEntry::getFiletype) + .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).editInLine().maxLength(26).done() .right("Meta").integer(GamedosFileEntry::getMeta, "$%04X").done() .right("First Track").integer(GamedosFileEntry::getFirstTrack, "%d").mode(DisplayMode.DETAIL).done() .right("First Sector").integer(GamedosFileEntry::getFirstSector, "%d").mode(DisplayMode.DETAIL).done() diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java index 09e5a463..8df125a6 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java @@ -19,21 +19,33 @@ public class DisplayColumn { private final ColumnDataType columnDataType; private final Function getValueFn; private final Function formatFn; + private final boolean editInLine; + private final int maxLength; + private final List options; - protected DisplayColumn(String title, ColumnAlignment alignment, Set modes, ColumnDataType columnDataType, - Function getValueFn, Function formatFn) { + private DisplayColumn(String title, ColumnAlignment alignment, Set modes, ColumnDataType columnDataType, + Function getValueFn, Function formatFn, boolean editInLine, + int maxLength, List options) { Objects.requireNonNull(title); Objects.requireNonNull(alignment); Objects.requireNonNull(modes); Objects.requireNonNull(columnDataType); Objects.requireNonNull(getValueFn); Objects.requireNonNull(formatFn); + Objects.requireNonNull(options); this.title = title; this.alignment = alignment; this.modes = modes; this.columnDataType = columnDataType; this.getValueFn = getValueFn; this.formatFn = formatFn; + this.editInLine = editInLine; + this.maxLength = maxLength; + this.options = Collections.unmodifiableList(options); + } + + public boolean isEditable() { + return editInLine || maxLength > 0 || !options.isEmpty(); } /// The column title. @@ -65,6 +77,14 @@ public class DisplayColumn { public String formatObject(Object object) { return formatFn.apply(object); } + /// Indicates this column can be edited directly in the file viewer. Typically just the name. + public boolean canEditInLine() { + return editInLine; + } + /// Get list of options (if any) when editing. + public List getOptions() { + return options; + } public static Builder builder(Class fileEntryType) { return new Builder<>(fileEntryType); @@ -107,6 +127,9 @@ public class DisplayColumn { private Function getValueFn; private Function formatFn; private DisplayMode[] modes; + private boolean editInLine = false; + private int maxLength = -1; + private final List options = new ArrayList<>(); private ColumnBuilder(Builder builder, ColumnAlignment alignment, String title) { Objects.requireNonNull(builder); @@ -167,6 +190,20 @@ public class DisplayColumn { this.modes = modes; return this; } + public ColumnBuilder editInLine() { + this.editInLine = true; + return this; + } + public ColumnBuilder maxLength(int maxLength) { + this.maxLength = maxLength; + return this; + } + public ColumnBuilder option(String value, String description) { + Objects.requireNonNull(value); + Objects.requireNonNull(description); + this.options.add(new ColumnOption(value, description)); + return this; + } public Builder done() { // A helper that allows a short-cut of 0 modes meaning ALL modes. @@ -176,7 +213,8 @@ public class DisplayColumn { // 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 convertedFn = f -> getValueFn.apply(builder.fileEntryType.cast(f)); - builder.columns.add(new DisplayColumn(title, alignment, Set.of(modes), columnDataType, convertedFn, formatFn)); + builder.columns.add(new DisplayColumn(title, alignment, Set.of(modes), columnDataType, convertedFn, formatFn, + editInLine, maxLength, options)); return builder; } From 20ce392414d1bbaa749278e5c0b03004ffc754c0 Mon Sep 17 00:00:00 2001 From: a2geek Date: Fri, 16 Oct 2026 15:31:01 -0500 Subject: [PATCH 04/11] Info pane can now edit and save changes. --- app/gui-javafx/src/main/java/module-info.java | 1 + .../org/applecommander/javafx/FileView.java | 58 ++++++++++- .../filter/CharacterRestrictionFilter.java | 22 +++++ .../javafx/filter/CompositeFilter.java | 25 +++++ .../javafx/filter/FixedLengthFilter.java | 22 +++++ .../os/gamedos/GamedosFileStore.java | 8 +- .../org/applecommander/ui/DisplayColumn.java | 96 ++++++++++++++++--- .../applecommander/ui/DisplayColumnTest.java | 19 ++++ 8 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java create mode 100644 app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CompositeFilter.java create mode 100644 app/gui-javafx/src/main/java/org/applecommander/javafx/filter/FixedLengthFilter.java create mode 100644 lib/ac-api/src/test/java/org/applecommander/ui/DisplayColumnTest.java diff --git a/app/gui-javafx/src/main/java/module-info.java b/app/gui-javafx/src/main/java/module-info.java index 4cb4ba0f..3b5a4466 100644 --- a/app/gui-javafx/src/main/java/module-info.java +++ b/app/gui-javafx/src/main/java/module-info.java @@ -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; } \ No newline at end of file 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 2fc5ac03..3ce20516 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,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,6 +28,7 @@ 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.input.*; @@ -45,6 +48,9 @@ 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; @@ -64,6 +70,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.*; @@ -208,9 +215,12 @@ public class FileView extends BorderPane { Button detailToggle = FxUtils.createSmallToggleButton("pane-right.png", "pane-left.png", showingDetailProperty); detailToggle.disableProperty().bind(fileTable.getSelectionModel().selectedItemProperty().isNull()); fileDetailGrid = new GridPane(); - fileDetailGrid.visibleProperty().bind(showingDetailProperty); - fileDetailGrid.managedProperty().bind(showingDetailProperty); - VBox detailPane = new VBox(detailToggle, fileDetailGrid); + 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)); @@ -240,6 +250,8 @@ public class FileView extends BorderPane { fileDetailGrid.setHgap(10); fileDetailGrid.setVgap(10); int row = 0; + SimpleListProperty 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(); @@ -249,10 +261,21 @@ public class FileView extends BorderPane { if (displayColumn.isEditable()) { if (displayColumn.getOptions().isEmpty()) { TextField field = new TextField(displayColumn.getDisplayText(fileEntry)); + List> filters = new ArrayList<>(); + if (displayColumn.getMaxLength() > 0) { + filters.add(new FixedLengthFilter(displayColumn.getMaxLength())); + } + if (displayColumn.getValidInputPattern() != null) { + filters.add(new CharacterRestrictionFilter(displayColumn.getValidInputPattern())); + } + field.setTextFormatter(new TextFormatter<>(new CompositeFilter(filters))); + field.setUserData(displayColumn); fileDetailGrid.addRow(row++, label, field); + editableFields.add(field); } else { ComboBox 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))) @@ -260,6 +283,7 @@ public class FileView extends BorderPane { combobox.setConverter(new StringConverter<>() { @Override public String toString(ColumnOption option) { + if (option == null) return ""; return String.format("%s - %s", option.value(), option.description()); } @@ -269,12 +293,40 @@ public class FileView extends BorderPane { } }); 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) { + case TextField textField -> textField.textProperty().isNotEmpty(); + case ComboBox comboBox -> comboBox.getSelectionModel().selectedItemProperty().isNotNull(); + 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) { diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java new file mode 100644 index 00000000..8b9490bb --- /dev/null +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java @@ -0,0 +1,22 @@ +package org.applecommander.javafx.filter; + +import javafx.scene.control.TextFormatter; + +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; + +public class CharacterRestrictionFilter implements UnaryOperator { + private final Pattern pattern; + + public CharacterRestrictionFilter(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public TextFormatter.Change apply(TextFormatter.Change change) { + if (pattern.matcher(change.getControlNewText()).matches()) { + return change; + } + return null; + } +} diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CompositeFilter.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CompositeFilter.java new file mode 100644 index 00000000..eb0ebd1b --- /dev/null +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CompositeFilter.java @@ -0,0 +1,25 @@ +package org.applecommander.javafx.filter; + +import javafx.scene.control.TextFormatter; + +import java.util.List; +import java.util.function.UnaryOperator; + +public class CompositeFilter implements UnaryOperator { + private final List> filters; + + public CompositeFilter(List> filters) { + this.filters = filters; + } + + @Override + public TextFormatter.Change apply(TextFormatter.Change change) { + for (UnaryOperator filter : filters) { + change = filter.apply(change); + if (change == null) { + break; + } + } + return change; + } +} diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/FixedLengthFilter.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/FixedLengthFilter.java new file mode 100644 index 00000000..453ccb20 --- /dev/null +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/FixedLengthFilter.java @@ -0,0 +1,22 @@ +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 { + 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; + } +} diff --git a/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java b/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java index d94d0a91..a2b35885 100644 --- a/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/os/gamedos/GamedosFileStore.java @@ -107,7 +107,7 @@ public class GamedosFileStore implements FileStore, GamedosConstants { @Override public List getDisplayColumns() { return DisplayColumn.builder(GamedosFileEntry.class) - .left("Type").string(GamedosFileEntry::getFiletype) + .left("Type").string(GamedosFileEntry::getFiletype, GamedosFileEntry::setFiletype) .option("A", "Applesoft BASIC") .option("B", "Binary") .option("I", "Integer BASIC") @@ -115,8 +115,10 @@ public class GamedosFileStore implements FileStore, GamedosConstants { .option("S", "System") .option("T", "Text") .done() - .left("Name").string(GamedosFileEntry::getName).editInLine().maxLength(26).done() - .right("Meta").integer(GamedosFileEntry::getMeta, "$%04X").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() diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java index 8df125a6..e0838e66 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java @@ -7,7 +7,9 @@ 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 FileStores in a /// dynamic fashion. The intent is to provide a flexible display interface @@ -19,33 +21,39 @@ public class DisplayColumn { private final ColumnDataType columnDataType; private final Function getValueFn; private final Function formatFn; + // These are all editable attributes + private final BiConsumer setValueFn; + private final boolean editable; private final boolean editInLine; private final int maxLength; private final List options; + private final Pattern validInputPattern; private DisplayColumn(String title, ColumnAlignment alignment, Set modes, ColumnDataType columnDataType, - Function getValueFn, Function formatFn, boolean editInLine, - int maxLength, List options) { + Function getValueFn, Function formatFn, BiConsumer setValueFn, + boolean editInLine, int maxLength, List options, 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); + // 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); - } - - public boolean isEditable() { - return editInLine || maxLength > 0 || !options.isEmpty(); + this.validInputPattern = validInputPattern; } /// The column title. @@ -77,14 +85,31 @@ public class DisplayColumn { 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 getOptions() { return options; } + /// 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 Builder builder(Class fileEntryType) { return new Builder<>(fileEntryType); @@ -118,18 +143,21 @@ public class DisplayColumn { } public static class ColumnBuilder { - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"); + 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 builder; private final ColumnAlignment alignment; private final String title; private ColumnDataType columnDataType; private Function getValueFn; - private Function formatFn; + private BiConsumer setValueFn; + private Function formatFn; private DisplayMode[] modes; private boolean editInLine = false; - private int maxLength = -1; + private int maxLength = 0; private final List options = new ArrayList<>(); + private Pattern validInputPattern; private ColumnBuilder(Builder builder, ColumnAlignment alignment, String title) { Objects.requireNonNull(builder); @@ -140,14 +168,20 @@ public class DisplayColumn { this.title = title; } - public ColumnBuilder string(Function getValueFn) { + public ColumnBuilder string(Function getValueFn) { Objects.requireNonNull(getValueFn); this.columnDataType = ColumnDataType.STRING; this.getValueFn = getValueFn; this.formatFn = s -> String.format("%s", s); return this; } - public ColumnBuilder integer(Function getValueFn, String fmt) { + public ColumnBuilder string(Function getValueFn, BiConsumer setValueFn) { + Objects.requireNonNull(getValueFn); + Objects.requireNonNull(setValueFn); + this.setValueFn = (e,t) -> setValueFn.accept(builder.fileEntryType.cast(e), t); + return string(getValueFn); + } + public ColumnBuilder integer(Function getValueFn, String fmt) { Objects.requireNonNull(getValueFn); Objects.requireNonNull(fmt); this.columnDataType = ColumnDataType.INTEGER; @@ -155,7 +189,14 @@ public class DisplayColumn { this.formatFn = n -> String.format(fmt, n); return this; } - public ColumnBuilder real(Function getValueFn, String fmt) { + public ColumnBuilder integer(Function getValueFn, String fmt, BiConsumer setValueFn) { + Objects.requireNonNull(getValueFn); + Objects.requireNonNull(fmt); + Objects.requireNonNull(setValueFn); + this.setValueFn = (e,t) -> setInteger(e, t, setValueFn); + return integer(getValueFn, fmt); + } + public ColumnBuilder real(Function getValueFn, String fmt) { Objects.requireNonNull(getValueFn); Objects.requireNonNull(fmt); this.columnDataType = ColumnDataType.REAL; @@ -163,7 +204,7 @@ public class DisplayColumn { this.formatFn = n -> String.format(fmt, n); return this; } - public ColumnBuilder date(Function getValueFn) { + public ColumnBuilder date(Function getValueFn) { Objects.requireNonNull(getValueFn); this.columnDataType = ColumnDataType.STRING; this.getValueFn = getValueFn; @@ -204,6 +245,10 @@ public class DisplayColumn { this.options.add(new ColumnOption(value, description)); return this; } + public ColumnBuilder restrictToHexValues() { + this.validInputPattern = VALID_HEX_NUMBER; + return this; + } public Builder done() { // A helper that allows a short-cut of 0 modes meaning ALL modes. @@ -214,7 +259,7 @@ public class DisplayColumn { // of awkward usages with FileStore etc. Or we do magic stuff like this to keep is simple. Function convertedFn = f -> getValueFn.apply(builder.fileEntryType.cast(f)); builder.columns.add(new DisplayColumn(title, alignment, Set.of(modes), columnDataType, convertedFn, formatFn, - editInLine, maxLength, options)); + setValueFn, editInLine, maxLength, options, validInputPattern)); return builder; } @@ -245,5 +290,28 @@ public class DisplayColumn { 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 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), 8); + } + 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); + } } } diff --git a/lib/ac-api/src/test/java/org/applecommander/ui/DisplayColumnTest.java b/lib/ac-api/src/test/java/org/applecommander/ui/DisplayColumnTest.java new file mode 100644 index 00000000..fe809b78 --- /dev/null +++ b/lib/ac-api/src/test/java/org/applecommander/ui/DisplayColumnTest.java @@ -0,0 +1,19 @@ +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()); + } +} From c596d40bda10832c9684b25668039f2f63f3dc16 Mon Sep 17 00:00:00 2001 From: a2geek Date: Fri, 16 Oct 2026 15:32:53 -0500 Subject: [PATCH 05/11] Adding license header. --- .../filter/CharacterRestrictionFilter.java | 19 +++++++++++++++++++ .../javafx/filter/CompositeFilter.java | 19 +++++++++++++++++++ .../javafx/filter/FixedLengthFilter.java | 19 +++++++++++++++++++ .../applecommander/ui/ColumnAlignment.java | 19 +++++++++++++++++++ .../org/applecommander/ui/ColumnDataType.java | 19 +++++++++++++++++++ .../org/applecommander/ui/ColumnOption.java | 19 +++++++++++++++++++ .../org/applecommander/ui/DisplayColumn.java | 19 +++++++++++++++++++ .../org/applecommander/ui/DisplayMode.java | 19 +++++++++++++++++++ .../applecommander/ui/DisplayColumnTest.java | 19 +++++++++++++++++++ 9 files changed, 171 insertions(+) diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java index 8b9490bb..fc4d5bd4 100644 --- a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java @@ -1,3 +1,22 @@ +/* + * 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; diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CompositeFilter.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CompositeFilter.java index eb0ebd1b..86d78938 100644 --- a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CompositeFilter.java +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CompositeFilter.java @@ -1,3 +1,22 @@ +/* + * 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; diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/FixedLengthFilter.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/FixedLengthFilter.java index 453ccb20..e856573e 100644 --- a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/FixedLengthFilter.java +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/FixedLengthFilter.java @@ -1,3 +1,22 @@ +/* + * 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; diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnAlignment.java b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnAlignment.java index 3f2b9e27..4aacae7c 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnAlignment.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnAlignment.java @@ -1,3 +1,22 @@ +/* + * 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. diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnDataType.java b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnDataType.java index f756bc4f..c60e9638 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnDataType.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnDataType.java @@ -1,3 +1,22 @@ +/* + * 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 { diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnOption.java b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnOption.java index 12c53e42..c23c559e 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/ColumnOption.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/ColumnOption.java @@ -1,3 +1,22 @@ +/* + * 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) { diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java index e0838e66..fcc95823 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java @@ -1,3 +1,22 @@ +/* + * 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; diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayMode.java b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayMode.java index 0cb85194..209fd319 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayMode.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayMode.java @@ -1,3 +1,22 @@ +/* + * 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. diff --git a/lib/ac-api/src/test/java/org/applecommander/ui/DisplayColumnTest.java b/lib/ac-api/src/test/java/org/applecommander/ui/DisplayColumnTest.java index fe809b78..a84dc1dc 100644 --- a/lib/ac-api/src/test/java/org/applecommander/ui/DisplayColumnTest.java +++ b/lib/ac-api/src/test/java/org/applecommander/ui/DisplayColumnTest.java @@ -1,3 +1,22 @@ +/* + * 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; From afc1e6637a2f163a6251eb80c9d4a4ff58b2ad4c Mon Sep 17 00:00:00 2001 From: a2geek Date: Fri, 16 Oct 2026 15:34:23 -0500 Subject: [PATCH 06/11] These images should have been committed. --- .../src/main/resources/images/pane-left.png | Bin 0 -> 1387 bytes .../src/main/resources/images/pane-right.png | Bin 0 -> 1312 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/gui-javafx/src/main/resources/images/pane-left.png create mode 100644 app/gui-javafx/src/main/resources/images/pane-right.png diff --git a/app/gui-javafx/src/main/resources/images/pane-left.png b/app/gui-javafx/src/main/resources/images/pane-left.png new file mode 100644 index 0000000000000000000000000000000000000000..43781d3d038e421b665ae321e6f3aede7168b441 GIT binary patch literal 1387 zcmV-x1(f=UP)QmR+p1WT#dHZ{5v<5Bea8t+!Hb^~o2} zhwLc&ASRiOmWJ$95RAqfN~~281ueb^-jG^K653`yna%8%O><^;&rWtXo9yo?=YKf) z&iU<`VK~eX);P4r*WVJ*+5xRgphE8K&)jAZt_QfAh%RTqkWK~6ya9xb-rvOZ2MGTb z`)UCJE;IiI(2p+T+Yd&wA4+EkWp}{#?E{_XI`~}z?v}6&3|*(fFi3C$1lXm8*SU3Q`HNcQ6iN-NJRT2&_K0VRaQ3`FXnUEXI1qAXOuA0 zGX(08z=wfT#C)8ITnU%J-2}Q=n)3qw_HIxTEHlJxFwu1cbV<0tOy16RN~;8=fIf5J zW&=Da!2(a@?d&Vk3Ls&oZ33kvm=}<9Y|E6OMkG?{eMEFn0w12^tm@L<^aTy~eV zLukAJ#SUZ)Il1h868HoOSo#k%ws6$(QPHX25+Eu{&@reY>iDSW)Nct86(#5xR1tN2 zRCMaM1c-_fbPTG9IzB2o^=l#^naT`FIyYuAi8AYgV$AAV77d z$lzhewsuQU0m+@2tI)hx@Q3%4Huv@fd!VH2k8FtMRe-d zC`}H{xuO0b$nEG>bC@liV}1Ts)#y1DmwLB z0z^d#ItEol9Um2)`Yi#Xq68g-Dx!{$icb9?0ZR@vn&~B}WGXWP!lK0n9mpsJBvY9~ zAncXEgLyKavu=@g2s*z&El8#^pMh|b1cmAIAtz@&ErE|(K!0Wz3=B))L*OmrwlyU| z0F4yTXFjskK;G-1akxZozgJ_2Y8mL~3R%P`f?D7koQLp*vMHk@AF#=sFVTGs`tIFyDgMT@R z|9ZZ#?HN_Q&>4A0RA_UDh^V4>gayUdA&n5gz}JR5w_COKi-ZdB=)f#UDJDEe zKwLUPP<|ca-8ITqTQ2wv0|y-Y(wBh!A+PmoOevALzYCom+d#Apgc|^gNv8tuqbUM5 zdN)i0{;c&!Q{D^lU>tLaKQd0wPCCvzr=%-{iUq7zS^`$gFKP)HLZ6AuF7QvT_7us4(&1F70$>i&9a&~s6JGfZnCQx<)VP&QW z)GdJr1LuhOI1{-NE`!$-=xS*$2>3%?pd^SGVoov9Ed;EV@FG)1C)*>fGPDA=StFZM z;7JKC@_5n7z9OwE7*@t1kSW1}fPBfxSrRk|!_4d7GLi+&|%D4@t0umJ0|ofTj3-@(Kn{mYnR(5@-lh|FSv-YE2&C z(>#;f?6<|Gh^2t2Ca;K{fG8)gh?Rh-C$ET=fV7!;4MdO0=qm_6c|~jl3|PCa>cPxe zXn$q8w8<-4E+FWAz}Bq~uj*KH#h<#}XZ54T49^UGQh4hp$pX@H0bvGUSoX67?3JK` zOIrnD>bLwppzSY$il`;TY6UD3rG!`tP(&pmwgMD2CA7nSKrIPvu^*tQwkNQn_XEn& zt%O+Y2L!zn42of9UItM=G5(pIncemA2XFo+of=|&5sZRFK%8!c8$lu!O(}GIC4*VSg;^|Q3)1$` z3%Jj|5uNx^f(rNx2C+1?H2+aq1%U}5?J6aAZrym{jZ+eMXay+JX67)6?vWq_x(B0m zS+npv{i=>g?b%<6>0<=CUc!LrCacSu0fT=&gR>jIcAP0)J>L<{zcA71juKIzXM_dK z+aawGz`(aDcW#Gn=`Z3dKrt{d(Al&0h9{V?mw+zm@Im`_h`MUjtQHkK&A@Xd=b9IR zXM7&(4>2UecxW|xy0?I63kbIX)FGW3)JszY^r{Oc0lzo;r7875JQ&AZ;?Gm(FHDw7 z@1Bz`9|8+lsU!kc>L(kNM1ZJhQ~n2LukL*h5)DrP000nVWkN?YGC46pG&D0sLP0b^ WIWb18M2k)U0000 Date: Fri, 16 Oct 2026 20:54:58 -0500 Subject: [PATCH 07/11] Fixing error display. --- .../src/main/java/org/applecommander/javafx/FileView.java | 2 +- .../src/main/java/org/applecommander/javafx/FxUtils.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 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 3ce20516..45ceb8e5 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 @@ -380,7 +380,7 @@ public class FileView extends BorderPane { } success = true; } - } catch (IOException e) { + } catch (Throwable e) { FxUtils.showErrorDialog("Unable to drop files", e); } event.setDropCompleted(success); diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java index 4c6d3cb6..d8f31d9d 100644 --- a/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java @@ -49,6 +49,7 @@ public class FxUtils { Button button = new Button(); button.setGraphic(vbox); button.setOnAction(eventHandler); + button.setFocusTraversable(false); return button; } @@ -65,6 +66,7 @@ public class FxUtils { button.setGraphic(vbox); button.setOnAction(eventHandler); button.setToggleGroup(toggleGroup); + button.setFocusTraversable(false); return button; } @@ -79,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))); @@ -121,7 +124,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(); } From 21d041ec13318ea0e2741f140e2d22febe248818 Mon Sep 17 00:00:00 2001 From: a2geek Date: Sat, 17 Oct 2026 12:02:08 -0500 Subject: [PATCH 08/11] Changed button toggle to be managed by the toggleState property instead of an action click. This way the button changes should a keypress or other action flip the toggle value. --- .../src/main/java/org/applecommander/javafx/FxUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java index d8f31d9d..f6218829 100644 --- a/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/FxUtils.java @@ -100,6 +100,8 @@ public class FxUtils { button.setGraphic(imageView); button.setOnAction(event -> { toggleState.setValue(!toggleState.getValue()); + }); + toggleState.addListener((_, _, _) -> { String imageUrl = toggleState.get() ? urlOn : urlOff; imageView.setImage(new Image(imageUrl(imageUrl))); }); From aeee5c009aac9bf5d0a4d297d1414e3aeeb17bb1 Mon Sep 17 00:00:00 2001 From: a2geek Date: Sat, 17 Oct 2026 12:02:33 -0500 Subject: [PATCH 09/11] Adding Shortcut+I to open information pane. --- .../src/main/java/org/applecommander/javafx/FileView.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 45ceb8e5..410116f3 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 @@ -101,6 +101,7 @@ public class FileView extends BorderPane { private final SimpleBooleanProperty supportsDirectories = new SimpleBooleanProperty(false); private final SimpleBooleanProperty supportsFileDeletion = new SimpleBooleanProperty(false); private final ObservableList fileEntries = FXCollections.observableArrayList(); + private final BooleanProperty showingDetailProperty = new SimpleBooleanProperty(false); public FileView(FileStoreWindow fileStoreWindow, ToolBar toolBar) { this.fileStoreWindow = fileStoreWindow; @@ -211,7 +212,6 @@ public class FileView extends BorderPane { fileTable.setPlaceholder(placeholder); setCenter(fileTable); - BooleanProperty showingDetailProperty = new SimpleBooleanProperty(false); Button detailToggle = FxUtils.createSmallToggleButton("pane-right.png", "pane-left.png", showingDetailProperty); detailToggle.disableProperty().bind(fileTable.getSelectionModel().selectedItemProperty().isNull()); fileDetailGrid = new GridPane(); @@ -616,6 +616,9 @@ public class FileView extends BorderPane { fileStoreWindow.addChange(); } } + else if (event.isShortcutDown() && event.getCode() == KeyCode.I) { + showingDetailProperty.set(!showingDetailProperty.get()); + } }); List rows = directory.getFiles().stream() From 7275eba2387db3d3987d540433a3c56907afc770 Mon Sep 17 00:00:00 2001 From: a2geek Date: Sat, 17 Oct 2026 19:15:17 -0500 Subject: [PATCH 10/11] Adding better input restriction; invalid fields are marked in read; the apply button is disabled if validation fails; adding 'CharacterMapper' to more generically handle the input restriction/mapping. --- .../org/applecommander/javafx/FileView.java | 21 ++++- .../filter/CharacterRestrictionFilter.java | 14 +-- .../applecommander/ui/CharacterMapper.java | 94 +++++++++++++++++++ .../org/applecommander/ui/DisplayColumn.java | 55 ++++++++++- .../ui/CharacterMapperTest.java | 50 ++++++++++ 5 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 lib/ac-api/src/main/java/org/applecommander/ui/CharacterMapper.java create mode 100644 lib/ac-api/src/test/java/org/applecommander/ui/CharacterMapperTest.java 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 410116f3..9068b69e 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 @@ -266,9 +266,18 @@ public class FileView extends BorderPane { filters.add(new FixedLengthFilter(displayColumn.getMaxLength())); } if (displayColumn.getValidInputPattern() != null) { - filters.add(new CharacterRestrictionFilter(displayColumn.getValidInputPattern())); + 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); @@ -280,6 +289,9 @@ public class FileView extends BorderPane { 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) { @@ -305,8 +317,11 @@ public class FileView extends BorderPane { // We have to handle every control created, however. :shrug: applyButton.disableProperty().bind( editableFields.stream().map(node -> switch (node) { - case TextField textField -> textField.textProperty().isNotEmpty(); - case ComboBox comboBox -> comboBox.getSelectionModel().selectedItemProperty().isNotNull(); + // 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 -> { diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java index fc4d5bd4..19489f07 100644 --- a/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/filter/CharacterRestrictionFilter.java @@ -20,22 +20,22 @@ package org.applecommander.javafx.filter; import javafx.scene.control.TextFormatter; +import org.applecommander.ui.CharacterMapper; import java.util.function.UnaryOperator; -import java.util.regex.Pattern; public class CharacterRestrictionFilter implements UnaryOperator { - private final Pattern pattern; + private final CharacterMapper characterMapper; - public CharacterRestrictionFilter(Pattern pattern) { - this.pattern = pattern; + public CharacterRestrictionFilter(CharacterMapper characterMapper) { + this.characterMapper = characterMapper; } @Override public TextFormatter.Change apply(TextFormatter.Change change) { - if (pattern.matcher(change.getControlNewText()).matches()) { - return change; + if (change.isAdded()) { + change.setText(characterMapper.map(change.getText())); } - return null; + return change; } } diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/CharacterMapper.java b/lib/ac-api/src/main/java/org/applecommander/ui/CharacterMapper.java new file mode 100644 index 00000000..284ad2bf --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/ui/CharacterMapper.java @@ -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()); + } + } +} diff --git a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java index fcc95823..711a9587 100644 --- a/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java +++ b/lib/ac-api/src/main/java/org/applecommander/ui/DisplayColumn.java @@ -30,9 +30,30 @@ import java.util.function.BiConsumer; import java.util.function.Function; import java.util.regex.Pattern; -/// A DisplayColumn supports formatted output from various FileStores in a +/// 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; @@ -46,11 +67,13 @@ public class DisplayColumn { private final boolean editInLine; private final int maxLength; private final List options; + private final CharacterMapper validCharacters; private final Pattern validInputPattern; private DisplayColumn(String title, ColumnAlignment alignment, Set modes, ColumnDataType columnDataType, Function getValueFn, Function formatFn, BiConsumer setValueFn, - boolean editInLine, int maxLength, List options, Pattern validInputPattern) { + boolean editInLine, int maxLength, List options, + CharacterMapper validCharacters, Pattern validInputPattern) { Objects.requireNonNull(title); Objects.requireNonNull(alignment); Objects.requireNonNull(modes); @@ -59,6 +82,7 @@ public class DisplayColumn { // 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; @@ -72,6 +96,7 @@ public class DisplayColumn { this.editInLine = editInLine; this.maxLength = maxLength; this.options = Collections.unmodifiableList(options); + this.validCharacters = validCharacters; this.validInputPattern = validInputPattern; } @@ -124,6 +149,10 @@ public class DisplayColumn { public List 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() { @@ -163,7 +192,7 @@ public class DisplayColumn { public static class ColumnBuilder { 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?$"); + public static final Pattern VALID_HEX_NUMBER = Pattern.compile("^(?i)0x[0-9a-f]+|[$][0-9a-f]+|[0-9a-f]+H?$"); private final Builder builder; private final ColumnAlignment alignment; @@ -176,6 +205,7 @@ public class DisplayColumn { private boolean editInLine = false; private int maxLength = 0; private final List options = new ArrayList<>(); + private CharacterMapper validCharacters; private Pattern validInputPattern; private ColumnBuilder(Builder builder, ColumnAlignment alignment, String title) { @@ -265,6 +295,16 @@ public class DisplayColumn { return this; } public ColumnBuilder 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; } @@ -274,11 +314,16 @@ public class DisplayColumn { 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 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, validInputPattern)); + setValueFn, editInLine, maxLength, options, + validCharacters, validInputPattern)); return builder; } @@ -317,7 +362,7 @@ public class DisplayColumn { value = Integer.parseInt(text.substring(2), 16); } else if (text.startsWith("$")) { - value = Integer.parseInt(text.substring(1), 8); + 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); diff --git a/lib/ac-api/src/test/java/org/applecommander/ui/CharacterMapperTest.java b/lib/ac-api/src/test/java/org/applecommander/ui/CharacterMapperTest.java new file mode 100644 index 00000000..0c5db26e --- /dev/null +++ b/lib/ac-api/src/test/java/org/applecommander/ui/CharacterMapperTest.java @@ -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 <<<")); + } +} From 48656a1838aab892650e84b18b095337703b216a Mon Sep 17 00:00:00 2001 From: a2geek Date: Sat, 17 Oct 2026 19:33:24 -0500 Subject: [PATCH 11/11] Re-enabling cell editing. --- .../org/applecommander/javafx/FileView.java | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 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 9068b69e..65bda13a 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 @@ -31,6 +31,7 @@ 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; @@ -556,23 +557,35 @@ public class FileView extends BorderPane { TableColumn column = switch (displayColumn.getDataType()) { case STRING -> { TableColumn stringColumn = new TableColumn<>(displayColumn.getTitle()); -// TODO -// if (displayColumn.editInline()) { -// stringColumn.setCellFactory(TextFieldTableCell.forTableColumn()); -// stringColumn.setEditable(true); -// stringColumn.setCellValueFactory(cell -> { -// SimpleStringProperty property = new SimpleStringProperty(displayColumn.getDisplayText(cell.getValue())); -// property.addListener((_, _, newValue) -> { -// displayColumn.setValueFn().accept(cell.getValue(), newValue); -// fileStoreWindow.addChange(); -// }); -// return property; -// }); -// editable = true; -// } else { - stringColumn.setCellValueFactory(cell -> - new SimpleStringProperty(displayColumn.getDisplayText(cell.getValue()))); -// } + 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.setOnEditCommit(event -> { + displayColumn.setValue(event.getRowValue(), event.getNewValue()); + }); + editable = true; + } yield stringColumn; } case INTEGER,REAL -> {