Dave Jarvis' Repositories

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

Add general key-value settings for document metadata

AuthorDaveJarvis <email>
Date2021-12-19 20:59:56 GMT-0800
Commitfb3891b9d251c95139946951dbcb343c90d310ea
Parentdea88dc
Delta1216 lines added, 917 lines removed, 299-line increase
src/main/resources/com/keenwrite/messages.properties
workspace.document=Document
+
+workspace.document.meta=Document Metadata
+workspace.document.meta.desc=Key-value pairs where values may contain variable references (e.g., '{{'book.title'}}').
+workspace.document.meta.title=Pairs
+
workspace.document.title=Title Name
workspace.document.title.desc=Full document title, or variable reference (e.g., '{{'book.title'}}').
workspace.document.email.title=Email
workspace.document.keywords=Keywords
-workspace.document.keywords.desc=Comma-separated words relating to subject matter, or variable reference.
+workspace.document.keywords.desc=Comma-separated words relating to the subject, or variable reference.
workspace.document.keywords.title=Words
workspace.document.copyright=Copyright
src/main/java/com/keenwrite/preferences/MapField.java
+package com.keenwrite.preferences;
+
+import com.dlsc.formsfx.model.structure.Field;
+import com.dlsc.formsfx.model.util.BindingMode;
+import javafx.beans.property.ObjectProperty;
+
+import java.util.Map;
+
+/**
+ * Responsible for binding a form field to a map of values that, ultimately,
+ * users may edit.
+ *
+ * @param <K> The type of key to store in the map.
+ * @param <V> The type of value to store in the map.
+ */
+public class MapField<K, V> extends Field<MapField<K, V>> {
+
+ private final MapProperty<K, V> mMapProperty;
+
+ public static <K, V> MapField<K, V> ofMapType(
+ final ObjectProperty<Map<K, V>> binding ) {
+
+ return new MapField<>( new MapProperty<>( binding.get() ) );
+ }
+
+ private MapField( final MapProperty<K, V> mapProperty ) {
+ assert mapProperty != null;
+
+ mMapProperty = mapProperty;
+ }
+
+ public MapProperty<K, V> mapProperty() {
+ return mMapProperty;
+ }
+
+ @Override
+ public void setBindingMode( final BindingMode newValue ) {
+ }
+
+ /**
+ * Answers whether the user input is valid.
+ *
+ * @return {@code true} Users may provide any strings.
+ */
+ @Override
+ protected boolean validate() {
+ return true;
+ }
+
+ @Override
+ public void persist() {
+ }
+
+ @Override
+ public void reset() {
+ }
+}
src/main/java/com/keenwrite/preferences/MapProperty.java
+package com.keenwrite.preferences;
+
+import javafx.beans.property.Property;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.Map;
+
+/**
+ * Responsible for wrapping a {@link Map} as an observable {@link Property}.
+ *
+ * @param <K> The type of key to insert into the {@link Map}.
+ * @param <V> The type of value to insert into the {@link Map}.
+ */
+public class MapProperty<K, V> extends SimpleObjectProperty<Map<K, V>> {
+
+ /**
+ * Use to instantiate a new {@link Property} that wraps a {@link Map}.
+ *
+ * @param map The {@link Map} to wrap as an observable {@link Property}.
+ */
+ public MapProperty( final Map<K, V> map ) {
+ super( map );
+ }
+
+ @Override
+ public String toString() {
+ return get().toString();
+ }
+}
src/main/java/com/keenwrite/preferences/PreferencesController.java
import java.io.File;
-
-import static com.dlsc.formsfx.model.structure.Field.ofStringType;
-import static com.dlsc.preferencesfx.PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
-import static com.keenwrite.Messages.get;
-import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
-import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
-import static com.keenwrite.preferences.SkinProperty.skinListProperty;
-import static com.keenwrite.preferences.AppKeys.*;
-import static javafx.scene.control.ButtonType.CANCEL;
-import static javafx.scene.control.ButtonType.OK;
-
-/**
- * Provides the ability for users to configure their preferences. This links
- * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC.
- */
-@SuppressWarnings( "SameParameterValue" )
-public final class PreferencesController {
-
- private final Workspace mWorkspace;
- private final PreferencesFx mPreferencesFx;
-
- public PreferencesController( final Workspace workspace ) {
- mWorkspace = workspace;
-
- // All properties must be initialized before creating the dialog.
- mPreferencesFx = createPreferencesFx();
-
- initKeyEventHandler( mPreferencesFx );
- }
-
- /**
- * Display the user preferences settings dialog (non-modal).
- */
- public void show() {
- getPreferencesFx().show( false );
- }
-
- /**
- * Call to persist the settings. Strictly speaking, this could watch on
- * all values for external changes then save automatically.
- */
- public void save() {
- getPreferencesFx().saveSettings();
- }
-
- /**
- * Delegates to the {@link PreferencesFx} event handler for monitoring
- * save events.
- *
- * @param eventHandler The handler to call when the preferences are saved.
- */
- public void addSaveEventHandler(
- final EventHandler<? super PreferencesFxEvent> eventHandler ) {
- getPreferencesFx().addEventHandler( EVENT_PREFERENCES_SAVED, eventHandler );
- }
-
- private StringField createFontNameField(
- final StringProperty fontName, final DoubleProperty fontSize ) {
- final var control = new SimpleFontControl( "Change" );
- control.fontSizeProperty().addListener( ( c, o, n ) -> {
- if( n != null ) {
- fontSize.set( n.doubleValue() );
- }
- } );
- return ofStringType( fontName ).render( control );
- }
-
- /**
- * Creates the preferences dialog based using {@link XmlStorageHandler} and
- * numerous {@link Category} objects.
- *
- * @return A component for editing preferences.
- * @throws RuntimeException Could not construct the {@link PreferencesFx}
- * object (e.g., illegal access permissions,
- * unmapped XML resource).
- */
- private PreferencesFx createPreferencesFx() {
- return PreferencesFx.of( createStorageHandler(), createCategories() )
- .instantPersistent( false )
- .dialogIcon( ICON_DIALOG );
- }
-
- private StorageHandler createStorageHandler() {
- return new XmlStorageHandler();
- }
-
- private Category[] createCategories() {
- return new Category[]{
- Category.of(
- get( KEY_DOC ),
- Group.of(
- get( KEY_DOC_TITLE ),
- Setting.of( label( KEY_DOC_TITLE ) ),
- Setting.of( title( KEY_DOC_TITLE ),
- stringProperty( KEY_DOC_TITLE ) )
- ),
- Group.of(
- get( KEY_DOC_AUTHOR ),
- Setting.of( label( KEY_DOC_AUTHOR ) ),
- Setting.of( title( KEY_DOC_AUTHOR ),
- stringProperty( KEY_DOC_AUTHOR ) )
- ),
- Group.of(
- get( KEY_DOC_BYLINE ),
- Setting.of( label( KEY_DOC_BYLINE ) ),
- Setting.of( title( KEY_DOC_BYLINE ),
- stringProperty( KEY_DOC_BYLINE ) )
- ),
- Group.of(
- get( KEY_DOC_ADDRESS ),
- Setting.of( label( KEY_DOC_ADDRESS ) ),
- createMultilineSetting( "Address", KEY_DOC_ADDRESS )
- ),
- Group.of(
- get( KEY_DOC_PHONE ),
- Setting.of( label( KEY_DOC_PHONE ) ),
- Setting.of( title( KEY_DOC_PHONE ),
- stringProperty( KEY_DOC_PHONE ) )
- ),
- Group.of(
- get( KEY_DOC_EMAIL ),
- Setting.of( label( KEY_DOC_EMAIL ) ),
- Setting.of( title( KEY_DOC_EMAIL ),
- stringProperty( KEY_DOC_EMAIL ) )
- ),
- Group.of(
- get( KEY_DOC_KEYWORDS ),
- Setting.of( label( KEY_DOC_KEYWORDS ) ),
- Setting.of( title( KEY_DOC_KEYWORDS ),
- stringProperty( KEY_DOC_KEYWORDS ) )
- ),
- Group.of(
- get( KEY_DOC_COPYRIGHT ),
- Setting.of( label( KEY_DOC_COPYRIGHT ) ),
- Setting.of( title( KEY_DOC_COPYRIGHT ),
- stringProperty( KEY_DOC_COPYRIGHT ) )
- ),
- Group.of(
- get( KEY_DOC_DATE ),
- Setting.of( label( KEY_DOC_DATE ) ),
- Setting.of( title( KEY_DOC_DATE ),
- stringProperty( KEY_DOC_DATE ) )
- )
- ),
- Category.of(
- get( KEY_TYPESET ),
- Group.of(
- get( KEY_TYPESET_CONTEXT ),
- Setting.of( label( KEY_TYPESET_CONTEXT_THEMES_PATH ) ),
- Setting.of( title( KEY_TYPESET_CONTEXT_THEMES_PATH ),
- fileProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ), true ),
- Setting.of( label( KEY_TYPESET_CONTEXT_CLEAN ) ),
- Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
- booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
- ),
- Group.of(
- get( KEY_TYPESET_TYPOGRAPHY ),
- Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
- Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
- booleanProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
- )
- ),
- Category.of(
- get( KEY_EDITOR ),
- Group.of(
- get( KEY_EDITOR_AUTOSAVE ),
- Setting.of( label( KEY_EDITOR_AUTOSAVE ) ),
- Setting.of( title( KEY_EDITOR_AUTOSAVE ),
- integerProperty( KEY_EDITOR_AUTOSAVE ) )
- )
- ),
- Category.of(
- get( KEY_R ),
- Group.of(
- get( KEY_R_DIR ),
- Setting.of( label( KEY_R_DIR,
- stringProperty( KEY_DEF_DELIM_BEGAN ).get(),
- stringProperty( KEY_DEF_DELIM_ENDED ).get() ) ),
- Setting.of( title( KEY_R_DIR ),
- fileProperty( KEY_R_DIR ), true )
- ),
- Group.of(
- get( KEY_R_SCRIPT ),
- Setting.of( label( KEY_R_SCRIPT ) ),
- createMultilineSetting( "Script", KEY_R_SCRIPT )
- ),
- Group.of(
- get( KEY_R_DELIM_BEGAN ),
- Setting.of( label( KEY_R_DELIM_BEGAN ) ),
- Setting.of( title( KEY_R_DELIM_BEGAN ),
- stringProperty( KEY_R_DELIM_BEGAN ) )
- ),
- Group.of(
- get( KEY_R_DELIM_ENDED ),
- Setting.of( label( KEY_R_DELIM_ENDED ) ),
- Setting.of( title( KEY_R_DELIM_ENDED ),
- stringProperty( KEY_R_DELIM_ENDED ) )
- )
- ),
- Category.of(
- get( KEY_IMAGES ),
- Group.of(
- get( KEY_IMAGES_DIR ),
- Setting.of( label( KEY_IMAGES_DIR ) ),
- Setting.of( title( KEY_IMAGES_DIR ),
- fileProperty( KEY_IMAGES_DIR ), true )
- ),
- Group.of(
- get( KEY_IMAGES_ORDER ),
- Setting.of( label( KEY_IMAGES_ORDER ) ),
- Setting.of( title( KEY_IMAGES_ORDER ),
- stringProperty( KEY_IMAGES_ORDER ) )
- ),
- Group.of(
- get( KEY_IMAGES_RESIZE ),
- Setting.of( label( KEY_IMAGES_RESIZE ) ),
- Setting.of( title( KEY_IMAGES_RESIZE ),
- booleanProperty( KEY_IMAGES_RESIZE ) )
- ),
- Group.of(
- get( KEY_IMAGES_SERVER ),
- Setting.of( label( KEY_IMAGES_SERVER ) ),
- Setting.of( title( KEY_IMAGES_SERVER ),
- stringProperty( KEY_IMAGES_SERVER ) )
- )
- ),
- Category.of(
- get( KEY_DEF ),
- Group.of(
- get( KEY_DEF_PATH ),
- Setting.of( label( KEY_DEF_PATH ) ),
- Setting.of( title( KEY_DEF_PATH ),
- fileProperty( KEY_DEF_PATH ), false )
- ),
- Group.of(
- get( KEY_DEF_DELIM_BEGAN ),
- Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
- Setting.of( title( KEY_DEF_DELIM_BEGAN ),
- stringProperty( KEY_DEF_DELIM_BEGAN ) )
- ),
- Group.of(
- get( KEY_DEF_DELIM_ENDED ),
- Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
- Setting.of( title( KEY_DEF_DELIM_ENDED ),
- stringProperty( KEY_DEF_DELIM_ENDED ) )
- )
- ),
- Category.of(
- get( KEY_UI_FONT ),
- Group.of(
- get( KEY_UI_FONT_EDITOR ),
- Setting.of( label( KEY_UI_FONT_EDITOR_NAME ) ),
- Setting.of( title( KEY_UI_FONT_EDITOR_NAME ),
- createFontNameField(
- stringProperty( KEY_UI_FONT_EDITOR_NAME ),
- doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ),
- stringProperty( KEY_UI_FONT_EDITOR_NAME ) ),
- Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
- Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
- doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
- ),
- Group.of(
- get( KEY_UI_FONT_PREVIEW ),
- Setting.of( label( KEY_UI_FONT_PREVIEW_NAME ) ),
- Setting.of( title( KEY_UI_FONT_PREVIEW_NAME ),
- createFontNameField(
- stringProperty( KEY_UI_FONT_PREVIEW_NAME ),
- doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
- stringProperty( KEY_UI_FONT_PREVIEW_NAME ) ),
- Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
- Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
- doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
- Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
- Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_NAME ),
- createFontNameField(
- stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ),
- doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
- stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
- Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
- Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_SIZE ),
- doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) )
- )
- ),
- Category.of(
- get( KEY_UI_SKIN ),
- Group.of(
- get( KEY_UI_SKIN_SELECTION ),
- Setting.of( label( KEY_UI_SKIN_SELECTION ) ),
- Setting.of( title( KEY_UI_SKIN_SELECTION ),
- skinListProperty(),
- skinProperty( KEY_UI_SKIN_SELECTION ) )
- ),
- Group.of(
- get( KEY_UI_SKIN_CUSTOM ),
- Setting.of( label( KEY_UI_SKIN_CUSTOM ) ),
- Setting.of( title( KEY_UI_SKIN_CUSTOM ),
- fileProperty( KEY_UI_SKIN_CUSTOM ), false )
- )
- ),
- Category.of(
- get( KEY_UI_PREVIEW ),
- Group.of(
- get( KEY_UI_PREVIEW_STYLESHEET ),
- Setting.of( label( KEY_UI_PREVIEW_STYLESHEET ) ),
- Setting.of( title( KEY_UI_PREVIEW_STYLESHEET ),
- fileProperty( KEY_UI_PREVIEW_STYLESHEET ), false )
- )
- ),
- Category.of(
- get( KEY_LANGUAGE ),
- Group.of(
- get( KEY_LANGUAGE_LOCALE ),
- Setting.of( label( KEY_LANGUAGE_LOCALE ) ),
- Setting.of( title( KEY_LANGUAGE_LOCALE ),
- localeListProperty(),
- localeProperty( KEY_LANGUAGE_LOCALE ) )
- )
- )};
- }
-
- @SuppressWarnings( "unchecked" )
- private Setting<StringField, StringProperty> createMultilineSetting(
- final String description, final Key property ) {
- final Setting<StringField, StringProperty> setting =
- Setting.of( description, stringProperty( property ) );
- final var field = setting.getElement();
- field.multiline( true );
-
- return setting;
- }
-
- private void initKeyEventHandler( final PreferencesFx preferences ) {
- final var view = preferences.getView();
- final var nodes = view.getChildrenUnmodifiable();
- final var master = (MasterDetailPane) nodes.get( 0 );
- final var detail = (NavigationView) master.getDetailNode();
- final var pane = (DialogPane) view.getParent();
-
- detail.setOnKeyReleased( ( key ) -> {
- switch( key.getCode() ) {
- case ENTER -> ((Button) pane.lookupButton( OK )).fire();
- case ESCAPE -> ((Button) pane.lookupButton( CANCEL )).fire();
- }
- } );
- }
-
- /**
- * Creates a label for the given key after interpolating its value.
- *
- * @param key The key to find in the resource bundle.
- * @return The value of the key as a label.
- */
- private Node label( final Key key ) {
- return label( key, (String[]) null );
- }
-
- private Node label( final Key key, final String... values ) {
- return new Label( get( key.toString() + ".desc", (Object[]) values ) );
- }
-
- private String title( final Key key ) {
- return get( key.toString() + ".title" );
- }
-
- private ObjectProperty<File> fileProperty( final Key key ) {
- return mWorkspace.fileProperty( key );
- }
-
- private StringProperty stringProperty( final Key key ) {
- return mWorkspace.stringProperty( key );
- }
-
- private BooleanProperty booleanProperty( final Key key ) {
- return mWorkspace.booleanProperty( key );
- }
-
- @SuppressWarnings( "SameParameterValue" )
- private IntegerProperty integerProperty( final Key key ) {
- return mWorkspace.integerProperty( key );
- }
-
- @SuppressWarnings( "SameParameterValue" )
- private DoubleProperty doubleProperty( final Key key ) {
- return mWorkspace.doubleProperty( key );
- }
-
- private ObjectProperty<String> skinProperty( final Key key ) {
- return mWorkspace.skinProperty( key );
- }
-
- private ObjectProperty<String> localeProperty( final Key key ) {
- return mWorkspace.localeProperty( key );
+import java.util.Map;
+
+import static com.dlsc.formsfx.model.structure.Field.ofStringType;
+import static com.dlsc.preferencesfx.PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
+import static com.keenwrite.preferences.AppKeys.*;
+import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
+import static com.keenwrite.preferences.MapField.ofMapType;
+import static com.keenwrite.preferences.SkinProperty.skinListProperty;
+import static javafx.scene.control.ButtonType.CANCEL;
+import static javafx.scene.control.ButtonType.OK;
+
+/**
+ * Provides the ability for users to configure their preferences. This links
+ * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC.
+ */
+@SuppressWarnings( "SameParameterValue" )
+public final class PreferencesController {
+
+ private final Workspace mWorkspace;
+ private final PreferencesFx mPreferencesFx;
+
+ public PreferencesController( final Workspace workspace ) {
+ mWorkspace = workspace;
+
+ // All properties must be initialized before creating the dialog.
+ mPreferencesFx = createPreferencesFx();
+
+ initKeyEventHandler( mPreferencesFx );
+ }
+
+ /**
+ * Display the user preferences settings dialog (non-modal).
+ */
+ public void show() {
+ getPreferencesFx().show( false );
+ }
+
+ /**
+ * Call to persist the settings. Strictly speaking, this could watch on
+ * all values for external changes then save automatically.
+ */
+ public void save() {
+ getPreferencesFx().saveSettings();
+ }
+
+ /**
+ * Delegates to the {@link PreferencesFx} event handler for monitoring
+ * save events.
+ *
+ * @param eventHandler The handler to call when the preferences are saved.
+ */
+ public void addSaveEventHandler(
+ final EventHandler<? super PreferencesFxEvent> eventHandler ) {
+ getPreferencesFx().addEventHandler( EVENT_PREFERENCES_SAVED, eventHandler );
+ }
+
+ private StringField createFontNameField(
+ final StringProperty fontName, final DoubleProperty fontSize ) {
+ final var control = new SimpleFontControl( "Change" );
+ control.fontSizeProperty().addListener( ( c, o, n ) -> {
+ if( n != null ) {
+ fontSize.set( n.doubleValue() );
+ }
+ } );
+ return ofStringType( fontName ).render( control );
+ }
+
+ private <K, V> MapField<K, V> createMapField(
+ final ObjectProperty<Map<K, V>> map ) {
+ final var control = new SimpleTableControl<>( map.get() );
+
+ return ofMapType( map ).render( control );
+ }
+
+ /**
+ * Creates the preferences dialog based using {@link XmlStorageHandler} and
+ * numerous {@link Category} objects.
+ *
+ * @return A component for editing preferences.
+ * @throws RuntimeException Could not construct the {@link PreferencesFx}
+ * object (e.g., illegal access permissions,
+ * unmapped XML resource).
+ */
+ private PreferencesFx createPreferencesFx() {
+ return PreferencesFx.of( createStorageHandler(), createCategories() )
+ .instantPersistent( false )
+ .dialogIcon( ICON_DIALOG );
+ }
+
+ private StorageHandler createStorageHandler() {
+ return new XmlStorageHandler();
+ }
+
+ private Category[] createCategories() {
+ return new Category[]{
+ Category.of(
+ get( KEY_DOC ),
+ Group.of(
+ get( KEY_DOC_META ),
+ Setting.of( label( KEY_DOC_META ) ),
+ Setting.of( title( KEY_DOC_META ),
+ createMapField( mapProperty( KEY_DOC_META ) ),
+ mapProperty( KEY_DOC_META ) )
+ ),
+ Group.of(
+ get( KEY_DOC_TITLE ),
+ Setting.of( label( KEY_DOC_TITLE ) ),
+ Setting.of( title( KEY_DOC_TITLE ),
+ stringProperty( KEY_DOC_TITLE ) )
+ ),
+ Group.of(
+ get( KEY_DOC_AUTHOR ),
+ Setting.of( label( KEY_DOC_AUTHOR ) ),
+ Setting.of( title( KEY_DOC_AUTHOR ),
+ stringProperty( KEY_DOC_AUTHOR ) )
+ ),
+ Group.of(
+ get( KEY_DOC_BYLINE ),
+ Setting.of( label( KEY_DOC_BYLINE ) ),
+ Setting.of( title( KEY_DOC_BYLINE ),
+ stringProperty( KEY_DOC_BYLINE ) )
+ ),
+ Group.of(
+ get( KEY_DOC_ADDRESS ),
+ Setting.of( label( KEY_DOC_ADDRESS ) ),
+ createMultilineSetting( "Address", KEY_DOC_ADDRESS )
+ ),
+ Group.of(
+ get( KEY_DOC_PHONE ),
+ Setting.of( label( KEY_DOC_PHONE ) ),
+ Setting.of( title( KEY_DOC_PHONE ),
+ stringProperty( KEY_DOC_PHONE ) )
+ ),
+ Group.of(
+ get( KEY_DOC_EMAIL ),
+ Setting.of( label( KEY_DOC_EMAIL ) ),
+ Setting.of( title( KEY_DOC_EMAIL ),
+ stringProperty( KEY_DOC_EMAIL ) )
+ ),
+ Group.of(
+ get( KEY_DOC_KEYWORDS ),
+ Setting.of( label( KEY_DOC_KEYWORDS ) ),
+ Setting.of( title( KEY_DOC_KEYWORDS ),
+ stringProperty( KEY_DOC_KEYWORDS ) )
+ ),
+ Group.of(
+ get( KEY_DOC_COPYRIGHT ),
+ Setting.of( label( KEY_DOC_COPYRIGHT ) ),
+ Setting.of( title( KEY_DOC_COPYRIGHT ),
+ stringProperty( KEY_DOC_COPYRIGHT ) )
+ ),
+ Group.of(
+ get( KEY_DOC_DATE ),
+ Setting.of( label( KEY_DOC_DATE ) ),
+ Setting.of( title( KEY_DOC_DATE ),
+ stringProperty( KEY_DOC_DATE ) )
+ )
+ ),
+ Category.of(
+ get( KEY_TYPESET ),
+ Group.of(
+ get( KEY_TYPESET_CONTEXT ),
+ Setting.of( label( KEY_TYPESET_CONTEXT_THEMES_PATH ) ),
+ Setting.of( title( KEY_TYPESET_CONTEXT_THEMES_PATH ),
+ fileProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ), true ),
+ Setting.of( label( KEY_TYPESET_CONTEXT_CLEAN ) ),
+ Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
+ booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
+ ),
+ Group.of(
+ get( KEY_TYPESET_TYPOGRAPHY ),
+ Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
+ Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
+ booleanProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
+ )
+ ),
+ Category.of(
+ get( KEY_EDITOR ),
+ Group.of(
+ get( KEY_EDITOR_AUTOSAVE ),
+ Setting.of( label( KEY_EDITOR_AUTOSAVE ) ),
+ Setting.of( title( KEY_EDITOR_AUTOSAVE ),
+ integerProperty( KEY_EDITOR_AUTOSAVE ) )
+ )
+ ),
+ Category.of(
+ get( KEY_R ),
+ Group.of(
+ get( KEY_R_DIR ),
+ Setting.of( label( KEY_R_DIR,
+ stringProperty( KEY_DEF_DELIM_BEGAN ).get(),
+ stringProperty( KEY_DEF_DELIM_ENDED ).get() ) ),
+ Setting.of( title( KEY_R_DIR ),
+ fileProperty( KEY_R_DIR ), true )
+ ),
+ Group.of(
+ get( KEY_R_SCRIPT ),
+ Setting.of( label( KEY_R_SCRIPT ) ),
+ createMultilineSetting( "Script", KEY_R_SCRIPT )
+ ),
+ Group.of(
+ get( KEY_R_DELIM_BEGAN ),
+ Setting.of( label( KEY_R_DELIM_BEGAN ) ),
+ Setting.of( title( KEY_R_DELIM_BEGAN ),
+ stringProperty( KEY_R_DELIM_BEGAN ) )
+ ),
+ Group.of(
+ get( KEY_R_DELIM_ENDED ),
+ Setting.of( label( KEY_R_DELIM_ENDED ) ),
+ Setting.of( title( KEY_R_DELIM_ENDED ),
+ stringProperty( KEY_R_DELIM_ENDED ) )
+ )
+ ),
+ Category.of(
+ get( KEY_IMAGES ),
+ Group.of(
+ get( KEY_IMAGES_DIR ),
+ Setting.of( label( KEY_IMAGES_DIR ) ),
+ Setting.of( title( KEY_IMAGES_DIR ),
+ fileProperty( KEY_IMAGES_DIR ), true )
+ ),
+ Group.of(
+ get( KEY_IMAGES_ORDER ),
+ Setting.of( label( KEY_IMAGES_ORDER ) ),
+ Setting.of( title( KEY_IMAGES_ORDER ),
+ stringProperty( KEY_IMAGES_ORDER ) )
+ ),
+ Group.of(
+ get( KEY_IMAGES_RESIZE ),
+ Setting.of( label( KEY_IMAGES_RESIZE ) ),
+ Setting.of( title( KEY_IMAGES_RESIZE ),
+ booleanProperty( KEY_IMAGES_RESIZE ) )
+ ),
+ Group.of(
+ get( KEY_IMAGES_SERVER ),
+ Setting.of( label( KEY_IMAGES_SERVER ) ),
+ Setting.of( title( KEY_IMAGES_SERVER ),
+ stringProperty( KEY_IMAGES_SERVER ) )
+ )
+ ),
+ Category.of(
+ get( KEY_DEF ),
+ Group.of(
+ get( KEY_DEF_PATH ),
+ Setting.of( label( KEY_DEF_PATH ) ),
+ Setting.of( title( KEY_DEF_PATH ),
+ fileProperty( KEY_DEF_PATH ), false )
+ ),
+ Group.of(
+ get( KEY_DEF_DELIM_BEGAN ),
+ Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
+ Setting.of( title( KEY_DEF_DELIM_BEGAN ),
+ stringProperty( KEY_DEF_DELIM_BEGAN ) )
+ ),
+ Group.of(
+ get( KEY_DEF_DELIM_ENDED ),
+ Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
+ Setting.of( title( KEY_DEF_DELIM_ENDED ),
+ stringProperty( KEY_DEF_DELIM_ENDED ) )
+ )
+ ),
+ Category.of(
+ get( KEY_UI_FONT ),
+ Group.of(
+ get( KEY_UI_FONT_EDITOR ),
+ Setting.of( label( KEY_UI_FONT_EDITOR_NAME ) ),
+ Setting.of( title( KEY_UI_FONT_EDITOR_NAME ),
+ createFontNameField(
+ stringProperty( KEY_UI_FONT_EDITOR_NAME ),
+ doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ),
+ stringProperty( KEY_UI_FONT_EDITOR_NAME ) ),
+ Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
+ Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
+ doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
+ ),
+ Group.of(
+ get( KEY_UI_FONT_PREVIEW ),
+ Setting.of( label( KEY_UI_FONT_PREVIEW_NAME ) ),
+ Setting.of( title( KEY_UI_FONT_PREVIEW_NAME ),
+ createFontNameField(
+ stringProperty( KEY_UI_FONT_PREVIEW_NAME ),
+ doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
+ stringProperty( KEY_UI_FONT_PREVIEW_NAME ) ),
+ Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
+ Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
+ doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
+ Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
+ Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_NAME ),
+ createFontNameField(
+ stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ),
+ doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
+ stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
+ Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
+ Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_SIZE ),
+ doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) )
+ )
+ ),
+ Category.of(
+ get( KEY_UI_SKIN ),
+ Group.of(
+ get( KEY_UI_SKIN_SELECTION ),
+ Setting.of( label( KEY_UI_SKIN_SELECTION ) ),
+ Setting.of( title( KEY_UI_SKIN_SELECTION ),
+ skinListProperty(),
+ skinProperty( KEY_UI_SKIN_SELECTION ) )
+ ),
+ Group.of(
+ get( KEY_UI_SKIN_CUSTOM ),
+ Setting.of( label( KEY_UI_SKIN_CUSTOM ) ),
+ Setting.of( title( KEY_UI_SKIN_CUSTOM ),
+ fileProperty( KEY_UI_SKIN_CUSTOM ), false )
+ )
+ ),
+ Category.of(
+ get( KEY_UI_PREVIEW ),
+ Group.of(
+ get( KEY_UI_PREVIEW_STYLESHEET ),
+ Setting.of( label( KEY_UI_PREVIEW_STYLESHEET ) ),
+ Setting.of( title( KEY_UI_PREVIEW_STYLESHEET ),
+ fileProperty( KEY_UI_PREVIEW_STYLESHEET ), false )
+ )
+ ),
+ Category.of(
+ get( KEY_LANGUAGE ),
+ Group.of(
+ get( KEY_LANGUAGE_LOCALE ),
+ Setting.of( label( KEY_LANGUAGE_LOCALE ) ),
+ Setting.of( title( KEY_LANGUAGE_LOCALE ),
+ localeListProperty(),
+ localeProperty( KEY_LANGUAGE_LOCALE ) )
+ )
+ )};
+ }
+
+ @SuppressWarnings( "unchecked" )
+ private Setting<StringField, StringProperty> createMultilineSetting(
+ final String description, final Key property ) {
+ final Setting<StringField, StringProperty> setting =
+ Setting.of( description, stringProperty( property ) );
+ final var field = setting.getElement();
+ field.multiline( true );
+
+ return setting;
+ }
+
+ private void initKeyEventHandler( final PreferencesFx preferences ) {
+ final var view = preferences.getView();
+ final var nodes = view.getChildrenUnmodifiable();
+ final var master = (MasterDetailPane) nodes.get( 0 );
+ final var detail = (NavigationView) master.getDetailNode();
+ final var pane = (DialogPane) view.getParent();
+
+ detail.setOnKeyReleased( ( key ) -> {
+ switch( key.getCode() ) {
+ case ENTER -> ((Button) pane.lookupButton( OK )).fire();
+ case ESCAPE -> ((Button) pane.lookupButton( CANCEL )).fire();
+ }
+ } );
+ }
+
+ /**
+ * Creates a label for the given key after interpolating its value.
+ *
+ * @param key The key to find in the resource bundle.
+ * @return The value of the key as a label.
+ */
+ private Node label( final Key key ) {
+ return label( key, (String[]) null );
+ }
+
+ private Node label( final Key key, final String... values ) {
+ return new Label( get( key.toString() + ".desc", (Object[]) values ) );
+ }
+
+ private String title( final Key key ) {
+ return get( key.toString() + ".title" );
+ }
+
+ private ObjectProperty<File> fileProperty( final Key key ) {
+ return mWorkspace.fileProperty( key );
+ }
+
+ private StringProperty stringProperty( final Key key ) {
+ return mWorkspace.stringProperty( key );
+ }
+
+ private BooleanProperty booleanProperty( final Key key ) {
+ return mWorkspace.booleanProperty( key );
+ }
+
+ @SuppressWarnings( "SameParameterValue" )
+ private IntegerProperty integerProperty( final Key key ) {
+ return mWorkspace.integerProperty( key );
+ }
+
+ @SuppressWarnings( "SameParameterValue" )
+ private DoubleProperty doubleProperty( final Key key ) {
+ return mWorkspace.doubleProperty( key );
+ }
+
+ private ObjectProperty<String> skinProperty( final Key key ) {
+ return mWorkspace.skinProperty( key );
+ }
+
+ private ObjectProperty<String> localeProperty( final Key key ) {
+ return mWorkspace.localeProperty( key );
+ }
+
+ private <K, V> MapProperty<K, V> mapProperty( final Key key ) {
+ return mWorkspace.mapProperty( key );
}
src/main/java/com/keenwrite/preferences/SimpleTableControl.java
+package com.keenwrite.preferences;
+
+import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Function;
+
+import static java.util.Arrays.asList;
+import static javafx.collections.FXCollections.observableArrayList;
+
+public class SimpleTableControl<K, V>
+ extends SimpleControl<MapField<K, V>, TableView<Entry<K, V>>> {
+
+ /**
+ * Data model for the table view, which must not be immutable.
+ */
+ private final Map<K, V> mMap;
+
+ public SimpleTableControl( final Map<K, V> map ) {
+ assert map != null;
+
+ mMap = map;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void initializeParts() {
+ super.initializeParts();
+
+ final var table = new TableView<>( observableArrayList( mMap.entrySet() ) );
+
+ table.setEditable( true );
+ table.getColumns().addAll(
+ asList(
+ createEditableColumnKey( table ),
+ createEditableColumnValue( table )
+ )
+ );
+
+ super.node = table;
+ }
+
+ private <T> TableColumn<Entry<K, V>, T> createColumn(
+ final TableView<Entry<K, V>> table,
+ final Function<Entry<K, V>, T> mapEntry,
+ final String label,
+ final double width
+ ) {
+ final var column = new TableColumn<Entry<K, V>, T>( label );
+
+ column.setEditable( true );
+ column.setResizable( true );
+ column.prefWidthProperty().bind( table.widthProperty().multiply( width ) );
+ column.setCellValueFactory(
+ cellData -> new SimpleObjectProperty<>(
+ mapEntry.apply( cellData.getValue() )
+ )
+ );
+
+ return column;
+ }
+
+ private TableColumn<Entry<K, V>, K> createEditableColumnKey(
+ final TableView<Entry<K, V>> table ) {
+ final var column = createColumn( table, Entry::getKey, "Key", .3 );
+
+ column.setOnEditCommit(
+ event -> {
+ final var tableEntry = event.getRowValue();
+ final var key = event.getNewValue();
+ final var value = tableEntry.getValue();
+ final var entry = new SimpleEntry<>( key, value );
+
+ mMap.remove( tableEntry.getKey() );
+ mMap.put( event.getNewValue(), tableEntry.getValue() );
+
+ final var items = event.getTableView().getItems();
+ final var rowIndex = event.getTablePosition().getRow();
+
+ items.set( rowIndex, entry );
+ }
+ );
+
+ return column;
+ }
+
+ private TableColumn<Entry<K, V>, V> createEditableColumnValue(
+ final TableView<Entry<K, V>> table ) {
+ final var column = createColumn( table, Entry::getValue, "Value", .7 );
+
+ column.setOnEditCommit(
+ event -> {
+ final var tableEntry = event.getRowValue();
+ tableEntry.setValue( event.getNewValue() );
+ }
+ );
+
+ return column;
+ }
+
+ @Override
+ public void layoutParts() {
+ }
+}
src/main/java/com/keenwrite/preferences/Workspace.java
import static java.util.Map.entry;
import static javafx.application.Platform.runLater;
-import static javafx.collections.FXCollections.observableArrayList;
-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 file name (no path), path, or directory.</dd>
- * <dt>Path</dt>
- * <dd>Fully qualified file name, which includes all parent directories.</dd>
- * <dt>Dir</dt>
- * <dd>Directory without file name ({@link File#isDirectory()} is true).</dd>
- * </dl>
- */
-public final class Workspace implements KeyConfiguration {
- private final Map<Key, Property<?>> VALUES = Map.ofEntries(
- entry( KEY_META_VERSION, asStringProperty( getVersion() ) ),
- entry( KEY_META_NAME, asStringProperty( "default" ) ),
-
- entry( KEY_DOC_TITLE, asStringProperty( "title" ) ),
- entry( KEY_DOC_AUTHOR, asStringProperty( getProperty( "user.name" ) ) ),
- entry( KEY_DOC_BYLINE, asStringProperty( getProperty( "user.name" ) ) ),
- entry( KEY_DOC_ADDRESS, asStringProperty( "" ) ),
- entry( KEY_DOC_PHONE, asStringProperty( "" ) ),
- entry( KEY_DOC_EMAIL, asStringProperty( "" ) ),
- entry( KEY_DOC_KEYWORDS, asStringProperty( "science, nature" ) ),
- entry( KEY_DOC_COPYRIGHT, asStringProperty( getYear() ) ),
- entry( KEY_DOC_DATE, asStringProperty( getDate() ) ),
-
- entry( KEY_EDITOR_AUTOSAVE, asIntegerProperty( 30 ) ),
-
- entry( KEY_R_SCRIPT, asStringProperty( "" ) ),
- entry( KEY_R_DIR, asFileProperty( USER_DIRECTORY ) ),
- entry( KEY_R_DELIM_BEGAN, asStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
- entry( KEY_R_DELIM_ENDED, asStringProperty( R_DELIM_ENDED_DEFAULT ) ),
-
- entry( KEY_IMAGES_DIR, asFileProperty( USER_DIRECTORY ) ),
- entry( KEY_IMAGES_ORDER, asStringProperty( PERSIST_IMAGES_DEFAULT ) ),
- entry( KEY_IMAGES_RESIZE, asBooleanProperty( true ) ),
- entry( KEY_IMAGES_SERVER, asStringProperty( DIAGRAM_SERVER_NAME ) ),
-
- entry( KEY_DEF_PATH, asFileProperty( DEFINITION_DEFAULT ) ),
- entry( KEY_DEF_DELIM_BEGAN, asStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
- entry( KEY_DEF_DELIM_ENDED, asStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
-
- entry( KEY_UI_RECENT_DIR, asFileProperty( USER_DIRECTORY ) ),
- entry( KEY_UI_RECENT_DOCUMENT, asFileProperty( DOCUMENT_DEFAULT ) ),
- entry( KEY_UI_RECENT_DEFINITION, asFileProperty( DEFINITION_DEFAULT ) ),
- entry( KEY_UI_RECENT_EXPORT, asFileProperty( PDF_DEFAULT ) ),
-
- //@formatter:off
- entry( KEY_UI_FONT_EDITOR_NAME, asStringProperty( FONT_NAME_EDITOR_DEFAULT ) ),
- entry( KEY_UI_FONT_EDITOR_SIZE, asDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ),
- entry( KEY_UI_FONT_PREVIEW_NAME, asStringProperty( FONT_NAME_PREVIEW_DEFAULT ) ),
- entry( KEY_UI_FONT_PREVIEW_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ),
- entry( KEY_UI_FONT_PREVIEW_MONO_NAME, asStringProperty( FONT_NAME_PREVIEW_MONO_NAME_DEFAULT ) ),
- entry( KEY_UI_FONT_PREVIEW_MONO_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT ) ),
-
- entry( KEY_UI_WINDOW_X, asDoubleProperty( WINDOW_X_DEFAULT ) ),
- entry( KEY_UI_WINDOW_Y, asDoubleProperty( WINDOW_Y_DEFAULT ) ),
- entry( KEY_UI_WINDOW_W, asDoubleProperty( WINDOW_W_DEFAULT ) ),
- entry( KEY_UI_WINDOW_H, asDoubleProperty( WINDOW_H_DEFAULT ) ),
- entry( KEY_UI_WINDOW_MAX, asBooleanProperty() ),
- entry( KEY_UI_WINDOW_FULL, asBooleanProperty() ),
-
- entry( KEY_UI_SKIN_SELECTION, asSkinProperty( SKIN_DEFAULT ) ),
- entry( KEY_UI_SKIN_CUSTOM, asFileProperty( SKIN_CUSTOM_DEFAULT ) ),
-
- entry( KEY_UI_PREVIEW_STYLESHEET, asFileProperty( PREVIEW_CUSTOM_DEFAULT ) ),
-
- entry( KEY_LANGUAGE_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ),
-
- entry( KEY_TYPESET_CONTEXT_CLEAN, asBooleanProperty( true ) ),
- entry( KEY_TYPESET_CONTEXT_THEMES_PATH, asFileProperty( USER_DIRECTORY ) ),
- entry( KEY_TYPESET_CONTEXT_THEME_SELECTION, asStringProperty( "boschet" ) ),
- entry( KEY_TYPESET_TYPOGRAPHY_QUOTES, asBooleanProperty( true ) )
- //@formatter:on
- );
-
- /**
- * Helps instantiate {@link Property} instances for XML configuration items.
- */
- private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
- Map.of(
- LocaleProperty.class, LocaleProperty::parseLocale,
- SimpleBooleanProperty.class, Boolean::parseBoolean,
- SimpleIntegerProperty.class, Integer::parseInt,
- SimpleDoubleProperty.class, Double::parseDouble,
- SimpleFloatProperty.class, Float::parseFloat,
- FileProperty.class, File::new
- );
-
- private static final Map<Class<?>, Function<String, Object>> MARSHALL =
- Map.of(
- LocaleProperty.class, LocaleProperty::toLanguageTag
- );
-
- private final Map<Key, SetProperty<?>> SETS = Map.ofEntries(
- entry(
- KEY_UI_FILES_PATH,
- new SimpleSetProperty<>( observableSet( new HashSet<>() ) )
- )
- );
-
- /**
- * Creates a new {@link Workspace} that will attempt to load a configuration
- * file. If the configuration file cannot be loaded, the workspace settings
- * will return default values. This allows unit tests to provide an instance
- * of {@link Workspace} when necessary without encountering failures.
- */
- public Workspace() {
- load( FILE_PREFERENCES );
- }
-
- /**
- * Creates a new {@link Workspace} that will attempt to load the given
- * configuration file.
- *
- * @param filename The file to load.
- */
- public Workspace( final String filename ) {
- load( filename );
- }
-
- /**
- * 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 ) {
- assert key != null;
- // The type that goes into the map must come out.
- return (U) VALUES.get( key );
- }
-
- /**
- * Creates an instance of {@link ObservableList} that is based on a
- * modifiable observable array list for the given items.
- *
- * @param items The items to wrap in an observable list.
- * @param <E> The type of items to add to the list.
- * @return An observable property that can have its contents modified.
- */
- public static <E> ObservableList<E> listProperty( final Set<E> items ) {
- return new SimpleListProperty<>( observableArrayList( items ) );
- }
-
- /**
- * Returns a list of values that represent a setting in the application that
- * the user may configure, either directly or indirectly. The property
- * returned is backed by a mutable {@link Set}.
- *
- * @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 ) {
- assert key != null;
- // The type that goes into the map must come out.
- return (SetProperty<T>) SETS.get( key );
- }
-
- private StringProperty asStringProperty( final String defaultValue ) {
- return new SimpleStringProperty( defaultValue );
- }
-
- private BooleanProperty asBooleanProperty() {
- return new SimpleBooleanProperty();
- }
-
- @SuppressWarnings( "SameParameterValue" )
- private BooleanProperty asBooleanProperty( final boolean defaultValue ) {
- return new SimpleBooleanProperty( defaultValue );
- }
-
- @SuppressWarnings( "SameParameterValue" )
- private IntegerProperty asIntegerProperty( final int defaultValue ) {
- return new SimpleIntegerProperty( defaultValue );
- }
-
- private DoubleProperty asDoubleProperty( final double defaultValue ) {
- return new SimpleDoubleProperty( defaultValue );
- }
-
- private FileProperty asFileProperty( final File defaultValue ) {
- return new FileProperty( defaultValue );
- }
-
- @SuppressWarnings( "SameParameterValue" )
- private LocaleProperty asLocaleProperty( final Locale defaultValue ) {
- return new LocaleProperty( defaultValue );
- }
-
- @SuppressWarnings( "SameParameterValue" )
- private SkinProperty asSkinProperty( final String defaultValue ) {
- return new SkinProperty( defaultValue );
- }
-
- /**
- * Returns the {@link String} {@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 StringProperty stringProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link Boolean} {@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 BooleanProperty booleanProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link Integer} {@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 IntegerProperty integerProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link Double} {@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 DoubleProperty doubleProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * 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 ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link Locale} {@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 LocaleProperty localeProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- public ObjectProperty<String> skinProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- @Override
- public String getString( final Key key ) {
- assert key != null;
- return stringProperty( key ).get();
- }
-
- /**
- * 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}.
- */
- @Override
- public boolean getBoolean( final Key key ) {
- assert key != null;
- return booleanProperty( key ).get();
- }
-
- /**
- * Returns the {@link Integer} 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}.
- */
- @Override
- public int getInteger( final Key key ) {
- assert key != null;
- return integerProperty( key ).get();
- }
-
- /**
- * 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}.
- */
- @Override
- public double getDouble( final Key key ) {
- assert key != null;
- return doubleProperty( key ).get();
- }
-
- /**
- * Returns the {@link File} 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}.
- */
- @Override
- public File getFile( final Key key ) {
- assert key != null;
- return fileProperty( key ).get();
- }
-
- /**
- * Returns the language locale setting for the
- * {@link AppKeys#KEY_LANGUAGE_LOCALE} key.
- *
- * @return The user's current locale setting.
- */
- public Locale getLocale() {
- return localeProperty( KEY_LANGUAGE_LOCALE ).toLocale();
- }
-
- private Sigils createSigils( final Key keyBegan, final Key keyEnded ) {
- assert keyBegan != null;
- assert keyEnded != null;
-
- return new Sigils( getString( keyBegan ), getString( keyEnded ) );
- }
-
- public SigilOperator createYamlSigilOperator() {
- return new YamlSigilOperator(
- createSigils( KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED )
- );
- }
-
- public SigilOperator createRSigilOperator() {
- return new RSigilOperator(
- createSigils( KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED ),
- createYamlSigilOperator()
- );
- }
-
- /**
- * 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 );
- }
- } )
- );
- }
-
- public void loadValueKeys( final Consumer<Key> consumer ) {
- VALUES.keySet().forEach( consumer );
- }
-
- public void loadSetKeys( final Consumer<Key> consumer ) {
- SETS.keySet().forEach( consumer );
- }
-
- /**
- * Calls the given consumer for all single-value keys. For lists, see
- * {@link #saveSets(BiConsumer)}.
- *
- * @param consumer Called to accept each preference key value.
- */
- public void saveValues( final BiConsumer<Key, Property<?>> consumer ) {
- VALUES.forEach( consumer );
- }
-
- /**
- * Calls the given consumer for all multi-value keys. For single items, see
- * {@link #saveValues(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 saveSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
- SETS.forEach( consumer );
- }
-
- /**
- * 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 );
- valuesProperty( KEY_META_VERSION ).setValue( getVersion() );
-
- saveValues( ( key, property ) ->
- config.setProperty( key.toString(), marshall( property ) )
- );
-
- saveSets( ( key, set ) -> {
- final var 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.
- *
- * @param filename The file containing user preferences to load.
- */
- private void load( final String filename ) {
- try {
- final var config = new Configurations().xml( filename );
-
- loadValueKeys( ( key ) -> {
- final var configValue = config.getProperty( key.toString() );
-
- // Allow other properties to load, even if any are missing.
- if( configValue != null ) {
- final var propertyValue = valuesProperty( key );
- propertyValue.setValue( unmarshall( propertyValue, configValue ) );
- }
- } );
-
- loadSetKeys( ( key ) -> {
- final var configSet =
- new LinkedHashSet<>( config.getList( key.toString() ) );
- final var propertySet = setsProperty( key );
- propertySet.setValue( observableSet( configSet ) );
+import static javafx.collections.FXCollections.*;
+
+/**
+ * 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 file name (no path), path, or directory.</dd>
+ * <dt>Path</dt>
+ * <dd>Fully qualified file name, which includes all parent directories.</dd>
+ * <dt>Dir</dt>
+ * <dd>Directory without file name ({@link File#isDirectory()} is true).</dd>
+ * </dl>
+ */
+public final class Workspace implements KeyConfiguration {
+ private final Map<Key, Property<?>> VALUES = Map.ofEntries(
+ entry( KEY_META_VERSION, asStringProperty( getVersion() ) ),
+ entry( KEY_META_NAME, asStringProperty( "default" ) ),
+
+ entry( KEY_DOC_TITLE, asStringProperty( "title" ) ),
+ entry( KEY_DOC_AUTHOR, asStringProperty( getProperty( "user.name" ) ) ),
+ entry( KEY_DOC_BYLINE, asStringProperty( getProperty( "user.name" ) ) ),
+ entry( KEY_DOC_ADDRESS, asStringProperty( "" ) ),
+ entry( KEY_DOC_PHONE, asStringProperty( "" ) ),
+ entry( KEY_DOC_EMAIL, asStringProperty( "" ) ),
+ entry( KEY_DOC_KEYWORDS, asStringProperty( "science, nature" ) ),
+ entry( KEY_DOC_COPYRIGHT, asStringProperty( getYear() ) ),
+ entry( KEY_DOC_DATE, asStringProperty( getDate() ) ),
+
+ entry( KEY_EDITOR_AUTOSAVE, asIntegerProperty( 30 ) ),
+
+ entry( KEY_R_SCRIPT, asStringProperty( "" ) ),
+ entry( KEY_R_DIR, asFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_R_DELIM_BEGAN, asStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
+ entry( KEY_R_DELIM_ENDED, asStringProperty( R_DELIM_ENDED_DEFAULT ) ),
+
+ entry( KEY_IMAGES_DIR, asFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_IMAGES_ORDER, asStringProperty( PERSIST_IMAGES_DEFAULT ) ),
+ entry( KEY_IMAGES_RESIZE, asBooleanProperty( true ) ),
+ entry( KEY_IMAGES_SERVER, asStringProperty( DIAGRAM_SERVER_NAME ) ),
+
+ entry( KEY_DEF_PATH, asFileProperty( DEFINITION_DEFAULT ) ),
+ entry( KEY_DEF_DELIM_BEGAN, asStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
+ entry( KEY_DEF_DELIM_ENDED, asStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
+
+ entry( KEY_UI_RECENT_DIR, asFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_UI_RECENT_DOCUMENT, asFileProperty( DOCUMENT_DEFAULT ) ),
+ entry( KEY_UI_RECENT_DEFINITION, asFileProperty( DEFINITION_DEFAULT ) ),
+ entry( KEY_UI_RECENT_EXPORT, asFileProperty( PDF_DEFAULT ) ),
+
+ //@formatter:off
+ entry( KEY_UI_FONT_EDITOR_NAME, asStringProperty( FONT_NAME_EDITOR_DEFAULT ) ),
+ entry( KEY_UI_FONT_EDITOR_SIZE, asDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ),
+ entry( KEY_UI_FONT_PREVIEW_NAME, asStringProperty( FONT_NAME_PREVIEW_DEFAULT ) ),
+ entry( KEY_UI_FONT_PREVIEW_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ),
+ entry( KEY_UI_FONT_PREVIEW_MONO_NAME, asStringProperty( FONT_NAME_PREVIEW_MONO_NAME_DEFAULT ) ),
+ entry( KEY_UI_FONT_PREVIEW_MONO_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT ) ),
+
+ entry( KEY_UI_WINDOW_X, asDoubleProperty( WINDOW_X_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_Y, asDoubleProperty( WINDOW_Y_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_W, asDoubleProperty( WINDOW_W_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_H, asDoubleProperty( WINDOW_H_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_MAX, asBooleanProperty() ),
+ entry( KEY_UI_WINDOW_FULL, asBooleanProperty() ),
+
+ entry( KEY_UI_SKIN_SELECTION, asSkinProperty( SKIN_DEFAULT ) ),
+ entry( KEY_UI_SKIN_CUSTOM, asFileProperty( SKIN_CUSTOM_DEFAULT ) ),
+
+ entry( KEY_UI_PREVIEW_STYLESHEET, asFileProperty( PREVIEW_CUSTOM_DEFAULT ) ),
+
+ entry( KEY_LANGUAGE_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ),
+
+ entry( KEY_TYPESET_CONTEXT_CLEAN, asBooleanProperty( true ) ),
+ entry( KEY_TYPESET_CONTEXT_THEMES_PATH, asFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_TYPESET_CONTEXT_THEME_SELECTION, asStringProperty( "boschet" ) ),
+ entry( KEY_TYPESET_TYPOGRAPHY_QUOTES, asBooleanProperty( true ) )
+ //@formatter:on
+ );
+
+ /**
+ * Helps instantiate {@link Property} instances for XML configuration items.
+ */
+ private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
+ Map.of(
+ LocaleProperty.class, LocaleProperty::parseLocale,
+ SimpleBooleanProperty.class, Boolean::parseBoolean,
+ SimpleIntegerProperty.class, Integer::parseInt,
+ SimpleDoubleProperty.class, Double::parseDouble,
+ SimpleFloatProperty.class, Float::parseFloat,
+ FileProperty.class, File::new
+ );
+
+ private static final Map<Class<?>, Function<String, Object>> MARSHALL =
+ Map.of(
+ LocaleProperty.class, LocaleProperty::toLanguageTag
+ );
+
+ private final Map<Key, SetProperty<?>> SETS = Map.ofEntries(
+ entry(
+ KEY_UI_FILES_PATH,
+ new SimpleSetProperty<>( observableSet( new HashSet<>() ) )
+ )
+ );
+
+ private final Map<Key, MapProperty<?, ?>> MAPS = Map.ofEntries(
+ entry(
+ KEY_DOC_META,
+ asMapProperty( new HashMap<String, String>() )
+ )
+ );
+
+ /**
+ * Creates a new {@link Workspace} that will attempt to load a configuration
+ * file. If the configuration file cannot be loaded, the workspace settings
+ * will return default values. This allows unit tests to provide an instance
+ * of {@link Workspace} when necessary without encountering failures.
+ */
+ public Workspace() {
+ load( FILE_PREFERENCES );
+ }
+
+ /**
+ * Creates a new {@link Workspace} that will attempt to load the given
+ * configuration file.
+ *
+ * @param filename The file to load.
+ */
+ public Workspace( final String filename ) {
+ load( filename );
+ }
+
+ /**
+ * 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 ) {
+ assert key != null;
+ // 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. The property
+ * returned is backed by a mutable {@link Set}.
+ *
+ * @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 ) {
+ assert key != null;
+ // The type that goes into the map must come out.
+ return (SetProperty<T>) SETS.get( key );
+ }
+
+ /**
+ * Returns the {@link Map} {@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 Map}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ @SuppressWarnings( "unchecked" )
+ public <K, V> MapProperty<K, V> mapProperty( final Key key ) {
+ return (MapProperty<K, V>) MAPS.get( key );
+ }
+
+ /**
+ * Creates an instance of {@link ObservableList} that is based on a
+ * modifiable observable array list for the given items.
+ *
+ * @param items The items to wrap in an observable list.
+ * @param <E> The type of items to add to the list.
+ * @return An observable property that can have its contents modified.
+ */
+ public static <E> ObservableList<E> listProperty( final Set<E> items ) {
+ return new SimpleListProperty<>( observableArrayList( items ) );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ private static StringProperty asStringProperty( final String value ) {
+ return new SimpleStringProperty( value );
+ }
+
+ private static BooleanProperty asBooleanProperty() {
+ return new SimpleBooleanProperty();
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static BooleanProperty asBooleanProperty( final boolean value ) {
+ return new SimpleBooleanProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static IntegerProperty asIntegerProperty( final int value ) {
+ return new SimpleIntegerProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ private static DoubleProperty asDoubleProperty( final double value ) {
+ return new SimpleDoubleProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ private static FileProperty asFileProperty( final File value ) {
+ return new FileProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static LocaleProperty asLocaleProperty( final Locale value ) {
+ return new LocaleProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ private static <K, V> MapProperty<K, V> asMapProperty(
+ final Map<K, V> value ) {
+ return new MapProperty<>( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static SkinProperty asSkinProperty( final String value ) {
+ return new SkinProperty( value );
+ }
+
+ /**
+ * Returns the {@link String} {@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 StringProperty stringProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link Boolean} {@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 BooleanProperty booleanProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link Integer} {@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 IntegerProperty integerProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link Double} {@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 DoubleProperty doubleProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * 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 ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link Locale} {@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 LocaleProperty localeProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ public ObjectProperty<String> skinProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ @Override
+ public String getString( final Key key ) {
+ assert key != null;
+ return stringProperty( key ).get();
+ }
+
+ /**
+ * 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}.
+ */
+ @Override
+ public boolean getBoolean( final Key key ) {
+ assert key != null;
+ return booleanProperty( key ).get();
+ }
+
+ /**
+ * Returns the {@link Integer} 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}.
+ */
+ @Override
+ public int getInteger( final Key key ) {
+ assert key != null;
+ return integerProperty( key ).get();
+ }
+
+ /**
+ * 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}.
+ */
+ @Override
+ public double getDouble( final Key key ) {
+ assert key != null;
+ return doubleProperty( key ).get();
+ }
+
+ /**
+ * Returns the {@link File} 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}.
+ */
+ @Override
+ public File getFile( final Key key ) {
+ assert key != null;
+ return fileProperty( key ).get();
+ }
+
+ /**
+ * Returns the language locale setting for the
+ * {@link AppKeys#KEY_LANGUAGE_LOCALE} key.
+ *
+ * @return The user's current locale setting.
+ */
+ public Locale getLocale() {
+ return localeProperty( KEY_LANGUAGE_LOCALE ).toLocale();
+ }
+
+ private Sigils createSigils( final Key keyBegan, final Key keyEnded ) {
+ assert keyBegan != null;
+ assert keyEnded != null;
+
+ return new Sigils( getString( keyBegan ), getString( keyEnded ) );
+ }
+
+ public SigilOperator createYamlSigilOperator() {
+ return new YamlSigilOperator(
+ createSigils( KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED )
+ );
+ }
+
+ public SigilOperator createRSigilOperator() {
+ return new RSigilOperator(
+ createSigils( KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED ),
+ createYamlSigilOperator()
+ );
+ }
+
+ /**
+ * 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 );
+ }
+ } )
+ );
+ }
+
+ public void loadValueKeys( final Consumer<Key> consumer ) {
+ VALUES.keySet().forEach( consumer );
+ }
+
+ public void loadSetKeys( final Consumer<Key> consumer ) {
+ SETS.keySet().forEach( consumer );
+ }
+
+ public void loadMapKeys( final Consumer<Key> consumer ) {
+ MAPS.keySet().forEach( consumer );
+ }
+
+ /**
+ * Calls the given consumer for all single-value keys. For lists, see
+ * {@link #saveSets(BiConsumer)}.
+ *
+ * @param consumer Called to accept each preference key value.
+ */
+ public void saveValues( final BiConsumer<Key, Property<?>> consumer ) {
+ VALUES.forEach( consumer );
+ }
+
+ /**
+ * Calls the given consumer for all multi-value keys. For single items, see
+ * {@link #saveValues(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 saveSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
+ SETS.forEach( consumer );
+ }
+
+ public void saveMaps( final BiConsumer<Key, MapProperty<?, ?>> consumer ) {
+ MAPS.forEach( consumer );
+ }
+
+ /**
+ * 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 );
+ valuesProperty( KEY_META_VERSION ).setValue( getVersion() );
+
+ saveValues(
+ ( key, property ) ->
+ config.setProperty( key.toString(), marshall( property ) )
+ );
+
+ saveSets(
+ ( key, set ) -> {
+ final var keyName = key.toString();
+ set.forEach( value -> config.addProperty( keyName, value ) );
+ }
+ );
+
+ saveMaps(
+ ( key, map ) -> {
+ final var keyName = key.toString();
+ map.get().forEach( ( k, v ) -> System.out.printf(
+ "SAVE (%s): %s == %s%n", keyName, k, v
+ ) );
+ }
+ );
+
+ 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.
+ *
+ * @param filename The file containing user preferences to load.
+ */
+ private void load( final String filename ) {
+ try {
+ final var config = new Configurations().xml( filename );
+
+ loadValueKeys( key -> {
+ final var configValue = config.getProperty( key.toString() );
+
+ // Allow other properties to load, even if any are missing.
+ if( configValue != null ) {
+ final var propertyValue = valuesProperty( key );
+ propertyValue.setValue( unmarshall( propertyValue, configValue ) );
+ }
+ } );
+
+ loadSetKeys( key -> {
+ final var configSet =
+ new LinkedHashSet<>( config.getList( key.toString() ) );
+ final var propertySet = setsProperty( key );
+ propertySet.setValue( observableSet( configSet ) );
+ } );
+
+ loadMapKeys( key -> {
+ final var configMap = new HashMap<String, String>();
+ final MapProperty<String, String> propertyMap = mapProperty( key );
+ System.out.println( "MAP = " + propertyMap );
+ propertyMap.setValue( observableMap( configMap ) );
} );
} catch( final Exception ex ) {