Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Persist configuration after changes

AuthorDaveJarvis <email>
Date2020-12-24 16:32:58 GMT-0800
Commit544fd814d3416a894e23e3943474c627f7b47462
Parentda22a91
Delta360 lines added, 371 lines removed, 11-line decrease
src/main/java/com/keenwrite/preferences/Workspace.java
import org.apache.commons.configuration2.XMLConfiguration;
import org.apache.commons.configuration2.builder.fluent.Configurations;
-import org.apache.commons.configuration2.ex.ConfigurationException;
-import org.apache.commons.configuration2.io.FileHandler;
-
-import java.io.File;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Map;
-import java.util.function.BiConsumer;
-import java.util.function.BooleanSupplier;
-import java.util.function.Consumer;
-import java.util.function.Function;
-
-import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
-import static com.keenwrite.Constants.*;
-import static com.keenwrite.Launcher.getVersion;
-import static com.keenwrite.StatusBarNotifier.clue;
-import static com.keenwrite.preferences.Key.key;
-import static java.util.Map.entry;
-import static javafx.application.Platform.runLater;
-import static javafx.collections.FXCollections.observableSet;
-
-/**
- * Responsible for defining behaviours for separate projects. A workspace has
- * the ability to save and restore a session, including the window dimensions,
- * tab setup, files, and user preferences.
- * <p>
- * The configuration must support hierarchical (nested) configuration nodes
- * to persist the user interface state. Although possible with a flat
- * configuration file, it's not nearly as simple or elegant.
- * </p>
- * <p>
- * Neither JSON nor HOCON support schema validation and versioning, which makes
- * XML the more suitable configuration file format. Schema validation and
- * versioning provide future-proofing and ease of reading and upgrading previous
- * versions of the configuration file.
- * </p>
- * <p>
- * Persistent preferences may be set directly by the user or indirectly by
- * the act of using the application.
- * </p>
- * <p>
- * Note the following definitions:
- * </p>
- * <dl>
- * <dt>File</dt>
- * <dd>References a filename (no path), path, or directory.</dd>
- * <dt>Path</dt>
- * <dd>Fully qualified filename, which includes all parent directories.</dd>
- * <dt>Dir</dt>
- * <dd>Directory without a filename ({@link File#isDirectory()} is true).</dd>
- * </dl>
- */
-public class Workspace {
- private static final Key KEY_ROOT = key( "workspace" );
-
- public static final Key KEY_META = key( KEY_ROOT, "meta" );
- public static final Key KEY_META_NAME = key( KEY_META, "name" );
- public static final Key KEY_META_VERSION = key( KEY_META, "version" );
-
- public static final Key KEY_R = key( KEY_ROOT, "r" );
- public static final Key KEY_R_SCRIPT = key( KEY_R, "script" );
- public static final Key KEY_R_DIR = key( KEY_R, "dir" );
- public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" );
- public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" );
- public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" );
-
- public static final Key KEY_IMAGES = key( KEY_ROOT, "images" );
- public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" );
- public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" );
-
- public static final Key KEY_DEF = key( KEY_ROOT, "definition" );
- public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" );
- public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" );
- public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" );
- public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" );
-
- //@formatter:off
- public static final Key KEY_UI = key( KEY_ROOT, "ui" );
-
- public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" );
- public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" );
- public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT,"document" );
- public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" );
-
- public static final Key KEY_UI_FILES = key( KEY_UI, "files" );
- public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" );
-
- public static final Key KEY_UI_FONT = key( KEY_UI, "font" );
- public static final Key KEY_UI_FONT_LOCALE = key( KEY_UI_FONT, "locale" );
- public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" );
- public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" );
- public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" );
- public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" );
-
- public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" );
- public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" );
- public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" );
- public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" );
- public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" );
- public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" );
- public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" );
-
- private final Map<Key, Property<?>> VALUES = Map.ofEntries(
- entry( KEY_META_VERSION, new SimpleStringProperty( getVersion() ) ),
- entry( KEY_META_NAME, new SimpleStringProperty( "default" ) ),
-
- entry( KEY_R_SCRIPT, new SimpleStringProperty( "" ) ),
- entry( KEY_R_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
- entry( KEY_R_DELIM_BEGAN, new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
- entry( KEY_R_DELIM_ENDED, new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ) ),
-
- entry( KEY_IMAGES_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
- entry( KEY_IMAGES_ORDER, new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ) ),
-
- entry( KEY_DEF_PATH, new SimpleFileProperty( DEFINITION_DEFAULT ) ),
- entry( KEY_DEF_DELIM_BEGAN, new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
- entry( KEY_DEF_DELIM_ENDED, new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
-
- entry( KEY_UI_RECENT_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
- entry( KEY_UI_RECENT_DOCUMENT, new SimpleFileProperty( DOCUMENT_DEFAULT ) ),
- entry( KEY_UI_RECENT_DEFINITION, new SimpleFileProperty( DEFINITION_DEFAULT ) ),
-
- entry( KEY_UI_FONT_LOCALE, new SimpleLocaleProperty( LOCALE_DEFAULT ) ),
- entry( KEY_UI_FONT_EDITOR_SIZE, new SimpleDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ),
- entry( KEY_UI_FONT_PREVIEW_SIZE, new SimpleDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ),
-
- entry( KEY_UI_WINDOW_X, new SimpleDoubleProperty( WINDOW_X_DEFAULT ) ),
- entry( KEY_UI_WINDOW_Y, new SimpleDoubleProperty( WINDOW_Y_DEFAULT ) ),
- entry( KEY_UI_WINDOW_W, new SimpleDoubleProperty( WINDOW_W_DEFAULT ) ),
- entry( KEY_UI_WINDOW_H, new SimpleDoubleProperty( WINDOW_H_DEFAULT ) ),
- entry( KEY_UI_WINDOW_MAX, new SimpleBooleanProperty() ),
- entry( KEY_UI_WINDOW_FULL, new SimpleBooleanProperty() )
- );
- //@formatter:on
-
- private final Map<Key, SetProperty<?>> SETS = Map.ofEntries(
- entry( KEY_UI_FILES_PATH, new SimpleSetProperty<>() )
- );
-
- /**
- * Helps instantiate {@link Property} instances for XML configuration items.
- */
- private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
- Map.of(
- SimpleLocaleProperty.class, Locale::forLanguageTag,
- SimpleBooleanProperty.class, Boolean::parseBoolean,
- SimpleDoubleProperty.class, Double::parseDouble,
- SimpleFloatProperty.class, Float::parseFloat,
- SimpleFileProperty.class, File::new
- );
-
- public Workspace() {
- load();
- }
-
- /**
- * Returns a value that represents a setting in the application that the user
- * may configure, either directly or indirectly.
- *
- * @param key The reference to the users' preference stored in deference
- * of app reëntrance.
- * @return An observable property to be persisted.
- */
- @SuppressWarnings("unchecked")
- public <T, U extends Property<T>> U valuesProperty( final Key key ) {
- // The type that goes into the map must come out.
- return (U) VALUES.get( key );
- }
-
- /**
- * Returns a list of values that represent a setting in the application that
- * the user may configure, either directly or indirectly.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return An observable property to be persisted.
- */
- @SuppressWarnings("unchecked")
- public <T> SetProperty<T> setsProperty( final Key key ) {
- // The type that goes into the map must come out.
- return (SetProperty<T>) SETS.get( key );
- }
-
- /**
- * Returns the {@link Double} preference value associated with the given
- * {@link Key}. The caller must be sure that the given {@link Key} is
- * associated with a value that matches the return type.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public double toDouble( final Key key ) {
- return (double) valuesProperty( key ).getValue();
- }
-
- /**
- * Returns the {@link Boolean} preference value associated with the given
- * {@link Key}. The caller must be sure that the given {@link Key} is
- * associated with a value that matches the return type.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public boolean toBoolean( final Key key ) {
- return (boolean) valuesProperty( key ).getValue();
- }
-
- /**
- * Returns the {@link File} {@link Property} associated with the given
- * {@link Key} from the internal list of preference values. The caller
- * must be sure that the given {@link Key} is associated with a {@link File}
- * {@link Property}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public ObjectProperty<File> fileProperty( final Key key ) {
- return valuesProperty( key );
- }
-
- public File toFile( final Key key ) {
- return fileProperty( key ).get();
- }
-
- public StringProperty stringProperty( final Key key ) {
- return valuesProperty( key );
- }
-
- public String toString( final Key key ) {
- return stringProperty( key ).get();
- }
-
- public Tokens toTokens( final Key began, final Key ended ) {
- return new Tokens( stringProperty( began ), stringProperty( ended ) );
- }
-
- @SuppressWarnings("SameParameterValue")
- public DoubleProperty doubleProperty( final Key key ) {
- return valuesProperty( key );
- }
-
- /**
- * Calls the given consumer for all single-value keys. For lists, see
- * {@link #consumeSets(BiConsumer)}.
- *
- * @param consumer Called to accept each preference key value.
- */
- public void consumeValues( final BiConsumer<Key, Property<?>> consumer ) {
- VALUES.forEach( consumer );
- }
-
- /**
- * Calls the given consumer for all multi-value keys. For single items, see
- * {@link #consumeValues(BiConsumer)}. Callers are responsible for iterating
- * over the list of items retrieved through this method.
- *
- * @param consumer Called to accept each preference key list.
- */
- public void consumeSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
- SETS.forEach( consumer );
- }
-
- public void consumeValueKeys( final Consumer<Key> consumer ) {
- VALUES.keySet().forEach( consumer );
- }
-
- public void consumeSetKeys( final Consumer<Key> consumer ) {
- SETS.keySet().forEach( consumer );
- }
-
- /**
- * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
- * providing a value of {@code true} for the {@link BooleanSupplier} to
- * indicate the property changes always take effect.
- *
- * @param key The value to bind to the internal key property.
- * @param property The external property value that sets the internal value.
- */
- public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
- listen( key, property, () -> true );
- }
-
- /**
- * Binds a read-only property to a value in the preferences. This allows
- * user interface properties to change and the preferences will be
- * synchronized automatically.
- * <p>
- * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
- * application window states are finished before assessing whether property
- * changes should be applied. Without this, exiting the application while the
- * window is maximized would persist the window's maximum dimensions,
- * preventing restoration to its prior, non-maximum size.
- * </p>
- *
- * @param key The value to bind to the internal key property.
- * @param property The external property value that sets the internal value.
- * @param enabled Indicates whether property changes should be applied.
- */
- public <T> void listen(
- final Key key,
- final ReadOnlyProperty<T> property,
- final BooleanSupplier enabled ) {
- property.addListener(
- ( c, o, n ) ->
- runLater( () -> {
- if( enabled.getAsBoolean() ) {
- valuesProperty( key ).setValue( n );
- }
- } )
- );
- }
-
- /**
- * Saves the current workspace.
- */
- public void save() {
- try {
- final var config = createConfiguration();
-
- // The root config key can only be set for an empty configuration file.
- config.setRootElementName( APP_TITLE_LOWERCASE );
-
- consumeValues( ( key, value ) -> config.setProperty(
- key.toString(), value.getValue() )
- );
-
- consumeSets(
- ( key, set ) -> {
- final String keyName = key.toString();
- set.forEach( ( value ) -> config.addProperty( keyName, value ) );
- }
- );
- new FileHandler( config ).save( FILE_PREFERENCES );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- /**
- * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file.
- * If not found, this will fall back to an empty configuration file, leaving
- * the application to fill in default values.
- */
- private void load() {
- try {
- final var config = createConfiguration();
-
- consumeValueKeys( ( key ) -> {
- final var configValue = config.getProperty( key.toString() );
- final var propertyValue = valuesProperty( key );
- propertyValue.setValue( unmarshall( propertyValue, configValue ) );
- } );
-
- consumeSetKeys( ( key ) -> {
- final var configList =
- new HashSet<>( config.getList( key.toString() ) );
- final var propertySet = setsProperty( key );
- propertySet.setValue( observableSet( configList ) );
- } );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- /**
- * Attempts to create a configuration that can read and write from the
- * {@link Constants#FILE_PREFERENCES} file.
- *
- * @return Configuration instance that can read and write project settings.
- */
- private XMLConfiguration createConfiguration() throws ConfigurationException {
- return new Configurations().xml( FILE_PREFERENCES );
+import org.apache.commons.configuration2.io.FileHandler;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
+import static com.keenwrite.Constants.*;
+import static com.keenwrite.Launcher.getVersion;
+import static com.keenwrite.StatusBarNotifier.clue;
+import static com.keenwrite.preferences.Key.key;
+import static java.util.Map.entry;
+import static javafx.application.Platform.runLater;
+import static javafx.collections.FXCollections.observableSet;
+
+/**
+ * Responsible for defining behaviours for separate projects. A workspace has
+ * the ability to save and restore a session, including the window dimensions,
+ * tab setup, files, and user preferences.
+ * <p>
+ * The configuration must support hierarchical (nested) configuration nodes
+ * to persist the user interface state. Although possible with a flat
+ * configuration file, it's not nearly as simple or elegant.
+ * </p>
+ * <p>
+ * Neither JSON nor HOCON support schema validation and versioning, which makes
+ * XML the more suitable configuration file format. Schema validation and
+ * versioning provide future-proofing and ease of reading and upgrading previous
+ * versions of the configuration file.
+ * </p>
+ * <p>
+ * Persistent preferences may be set directly by the user or indirectly by
+ * the act of using the application.
+ * </p>
+ * <p>
+ * Note the following definitions:
+ * </p>
+ * <dl>
+ * <dt>File</dt>
+ * <dd>References a filename (no path), path, or directory.</dd>
+ * <dt>Path</dt>
+ * <dd>Fully qualified filename, which includes all parent directories.</dd>
+ * <dt>Dir</dt>
+ * <dd>Directory without a filename ({@link File#isDirectory()} is true).</dd>
+ * </dl>
+ */
+public class Workspace {
+ private static final Key KEY_ROOT = key( "workspace" );
+
+ public static final Key KEY_META = key( KEY_ROOT, "meta" );
+ public static final Key KEY_META_NAME = key( KEY_META, "name" );
+ public static final Key KEY_META_VERSION = key( KEY_META, "version" );
+
+ public static final Key KEY_R = key( KEY_ROOT, "r" );
+ public static final Key KEY_R_SCRIPT = key( KEY_R, "script" );
+ public static final Key KEY_R_DIR = key( KEY_R, "dir" );
+ public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" );
+ public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" );
+ public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" );
+
+ public static final Key KEY_IMAGES = key( KEY_ROOT, "images" );
+ public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" );
+ public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" );
+
+ public static final Key KEY_DEF = key( KEY_ROOT, "definition" );
+ public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" );
+ public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" );
+ public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" );
+ public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" );
+
+ //@formatter:off
+ public static final Key KEY_UI = key( KEY_ROOT, "ui" );
+
+ public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" );
+ public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" );
+ public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT,"document" );
+ public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" );
+
+ public static final Key KEY_UI_FILES = key( KEY_UI, "files" );
+ public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" );
+
+ public static final Key KEY_UI_FONT = key( KEY_UI, "font" );
+ public static final Key KEY_UI_FONT_LOCALE = key( KEY_UI_FONT, "locale" );
+ public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" );
+ public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" );
+ public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" );
+ public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" );
+
+ public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" );
+ public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" );
+ public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" );
+ public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" );
+ public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" );
+ public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" );
+ public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" );
+
+ private final Map<Key, Property<?>> VALUES = Map.ofEntries(
+ entry( KEY_META_VERSION, new SimpleStringProperty( getVersion() ) ),
+ entry( KEY_META_NAME, new SimpleStringProperty( "default" ) ),
+
+ entry( KEY_R_SCRIPT, new SimpleStringProperty( "" ) ),
+ entry( KEY_R_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_R_DELIM_BEGAN, new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
+ entry( KEY_R_DELIM_ENDED, new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ) ),
+
+ entry( KEY_IMAGES_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_IMAGES_ORDER, new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ) ),
+
+ entry( KEY_DEF_PATH, new SimpleFileProperty( DEFINITION_DEFAULT ) ),
+ entry( KEY_DEF_DELIM_BEGAN, new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
+ entry( KEY_DEF_DELIM_ENDED, new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
+
+ entry( KEY_UI_RECENT_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_UI_RECENT_DOCUMENT, new SimpleFileProperty( DOCUMENT_DEFAULT ) ),
+ entry( KEY_UI_RECENT_DEFINITION, new SimpleFileProperty( DEFINITION_DEFAULT ) ),
+
+ entry( KEY_UI_FONT_LOCALE, new SimpleLocaleProperty( LOCALE_DEFAULT ) ),
+ entry( KEY_UI_FONT_EDITOR_SIZE, new SimpleDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ),
+ entry( KEY_UI_FONT_PREVIEW_SIZE, new SimpleDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ),
+
+ entry( KEY_UI_WINDOW_X, new SimpleDoubleProperty( WINDOW_X_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_Y, new SimpleDoubleProperty( WINDOW_Y_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_W, new SimpleDoubleProperty( WINDOW_W_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_H, new SimpleDoubleProperty( WINDOW_H_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_MAX, new SimpleBooleanProperty() ),
+ entry( KEY_UI_WINDOW_FULL, new SimpleBooleanProperty() )
+ );
+ //@formatter:on
+
+ private final Map<Key, SetProperty<?>> SETS = Map.ofEntries(
+ entry( KEY_UI_FILES_PATH, new SimpleSetProperty<>() )
+ );
+
+ /**
+ * Helps instantiate {@link Property} instances for XML configuration items.
+ */
+ private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
+ Map.of(
+ SimpleLocaleProperty.class, Locale::forLanguageTag,
+ SimpleBooleanProperty.class, Boolean::parseBoolean,
+ SimpleDoubleProperty.class, Double::parseDouble,
+ SimpleFloatProperty.class, Float::parseFloat,
+ SimpleFileProperty.class, File::new
+ );
+
+ public Workspace() {
+ load();
+ }
+
+ /**
+ * Returns a value that represents a setting in the application that the user
+ * may configure, either directly or indirectly.
+ *
+ * @param key The reference to the users' preference stored in deference
+ * of app reëntrance.
+ * @return An observable property to be persisted.
+ */
+ @SuppressWarnings("unchecked")
+ public <T, U extends Property<T>> U valuesProperty( final Key key ) {
+ // The type that goes into the map must come out.
+ return (U) VALUES.get( key );
+ }
+
+ /**
+ * Returns a list of values that represent a setting in the application that
+ * the user may configure, either directly or indirectly.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return An observable property to be persisted.
+ */
+ @SuppressWarnings("unchecked")
+ public <T> SetProperty<T> setsProperty( final Key key ) {
+ // The type that goes into the map must come out.
+ return (SetProperty<T>) SETS.get( key );
+ }
+
+ /**
+ * Returns the {@link Double} preference value associated with the given
+ * {@link Key}. The caller must be sure that the given {@link Key} is
+ * associated with a value that matches the return type.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public double toDouble( final Key key ) {
+ return (double) valuesProperty( key ).getValue();
+ }
+
+ /**
+ * Returns the {@link Boolean} preference value associated with the given
+ * {@link Key}. The caller must be sure that the given {@link Key} is
+ * associated with a value that matches the return type.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public boolean toBoolean( final Key key ) {
+ return (boolean) valuesProperty( key ).getValue();
+ }
+
+ /**
+ * Returns the {@link File} {@link Property} associated with the given
+ * {@link Key} from the internal list of preference values. The caller
+ * must be sure that the given {@link Key} is associated with a {@link File}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public ObjectProperty<File> fileProperty( final Key key ) {
+ return valuesProperty( key );
+ }
+
+ public File toFile( final Key key ) {
+ return fileProperty( key ).get();
+ }
+
+ public StringProperty stringProperty( final Key key ) {
+ return valuesProperty( key );
+ }
+
+ public String toString( final Key key ) {
+ return stringProperty( key ).get();
+ }
+
+ public Tokens toTokens( final Key began, final Key ended ) {
+ return new Tokens( stringProperty( began ), stringProperty( ended ) );
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ public DoubleProperty doubleProperty( final Key key ) {
+ return valuesProperty( key );
+ }
+
+ /**
+ * Calls the given consumer for all single-value keys. For lists, see
+ * {@link #consumeSets(BiConsumer)}.
+ *
+ * @param consumer Called to accept each preference key value.
+ */
+ public void consumeValues( final BiConsumer<Key, Property<?>> consumer ) {
+ VALUES.forEach( consumer );
+ }
+
+ /**
+ * Calls the given consumer for all multi-value keys. For single items, see
+ * {@link #consumeValues(BiConsumer)}. Callers are responsible for iterating
+ * over the list of items retrieved through this method.
+ *
+ * @param consumer Called to accept each preference key list.
+ */
+ public void consumeSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
+ SETS.forEach( consumer );
+ }
+
+ public void consumeValueKeys( final Consumer<Key> consumer ) {
+ VALUES.keySet().forEach( consumer );
+ }
+
+ public void consumeSetKeys( final Consumer<Key> consumer ) {
+ SETS.keySet().forEach( consumer );
+ }
+
+ /**
+ * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
+ * providing a value of {@code true} for the {@link BooleanSupplier} to
+ * indicate the property changes always take effect.
+ *
+ * @param key The value to bind to the internal key property.
+ * @param property The external property value that sets the internal value.
+ */
+ public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
+ listen( key, property, () -> true );
+ }
+
+ /**
+ * Binds a read-only property to a value in the preferences. This allows
+ * user interface properties to change and the preferences will be
+ * synchronized automatically.
+ * <p>
+ * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
+ * application window states are finished before assessing whether property
+ * changes should be applied. Without this, exiting the application while the
+ * window is maximized would persist the window's maximum dimensions,
+ * preventing restoration to its prior, non-maximum size.
+ * </p>
+ *
+ * @param key The value to bind to the internal key property.
+ * @param property The external property value that sets the internal value.
+ * @param enabled Indicates whether property changes should be applied.
+ */
+ public <T> void listen(
+ final Key key,
+ final ReadOnlyProperty<T> property,
+ final BooleanSupplier enabled ) {
+ property.addListener(
+ ( c, o, n ) ->
+ runLater( () -> {
+ if( enabled.getAsBoolean() ) {
+ valuesProperty( key ).setValue( n );
+ }
+ } )
+ );
+ }
+
+ /**
+ * Saves the current workspace.
+ */
+ public void save() {
+ try {
+ final var config = new XMLConfiguration();
+
+ // The root config key can only be set for an empty configuration file.
+ config.setRootElementName( APP_TITLE_LOWERCASE );
+
+ consumeValues( ( key, value ) -> config.setProperty(
+ key.toString(), value.getValue() )
+ );
+
+ consumeSets(
+ ( key, set ) -> {
+ final String keyName = key.toString();
+ set.forEach( ( value ) -> config.addProperty( keyName, value ) );
+ }
+ );
+ new FileHandler( config ).save( FILE_PREFERENCES );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ /**
+ * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file.
+ * If not found, this will fall back to an empty configuration file, leaving
+ * the application to fill in default values.
+ */
+ private void load() {
+ try {
+ final var config = new Configurations().xml( FILE_PREFERENCES );
+
+ consumeValueKeys( ( key ) -> {
+ final var configValue = config.getProperty( key.toString() );
+ final var propertyValue = valuesProperty( key );
+ propertyValue.setValue( unmarshall( propertyValue, configValue ) );
+ } );
+
+ consumeSetKeys( ( key ) -> {
+ final var configList =
+ new HashSet<>( config.getList( key.toString() ) );
+ final var propertySet = setsProperty( key );
+ propertySet.setValue( observableSet( configList ) );
+ } );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
}