Dave Jarvis' Repositories

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

Update table field to persist list

AuthorDaveJarvis <email>
Date2021-12-23 01:08:49 GMT-0800
Commit97661c4b65298584571e0e31dcbbf255496abd55
Parent59741fc
Delta955 lines added, 854 lines removed, 101-line increase
src/main/java/com/keenwrite/preferences/PreferencesController.java
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
import static com.keenwrite.preferences.AppKeys.*;
-import static com.keenwrite.preferences.TableField.ofListType;
-import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
-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> TableField<Entry<K, V>> createListEntryField() {
- final TableField<Entry<K, V>> field = ofListType();
- return field.render( new SimpleTableControl<>() );
- }
-
- /**
- * 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 ),
- createListEntryField(),
- listEntryProperty( 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;
- }
-
- /**
- * Map ENTER and ESCAPE keys to OK and CANCEL buttons, respectively.
- */
- 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 <E> SetProperty<E> listEntryProperty( final Key key ) {
- return mWorkspace.setsProperty( key );
+import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
+import static com.keenwrite.preferences.SkinProperty.skinListProperty;
+import static com.keenwrite.preferences.TableField.ofListType;
+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 );
+ }
+
+ /**
+ * Convenience method to create a helper class for the user interface. This
+ * establishes a key-value pair for the view.
+ *
+ * @param persist A reference to the values that will be persisted.
+ * @param <K> The type of key, usually a string.
+ * @param <V> The type of value, usually a string.
+ * @return UI data model container that may update the persistent state.
+ */
+ private <K, V> TableField<Entry<K, V>> createTableField(
+ final ListProperty<Entry<K, V>> persist ) {
+ return ofListType( persist ).render( new SimpleTableControl<>() );
+ }
+
+ /**
+ * 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 ),
+ createTableField( listEntryProperty( KEY_DOC_META ) ),
+ listEntryProperty( 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;
+ }
+
+ /**
+ * Map ENTER and ESCAPE keys to OK and CANCEL buttons, respectively.
+ */
+ 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 <E> ListProperty<E> listEntryProperty( final Key key ) {
+ return mWorkspace.listsProperty( key );
}
src/main/java/com/keenwrite/preferences/TableField.java
import com.dlsc.formsfx.model.util.BindingMode;
import javafx.beans.property.ListProperty;
+import javafx.beans.property.Property;
import javafx.beans.property.SimpleListProperty;
import java.util.ArrayList;
+import static com.dlsc.formsfx.model.util.BindingMode.CONTINUOUS;
import static javafx.collections.FXCollections.observableList;
/**
* Responsible for binding a form field to a map of values that, ultimately,
* users may edit.
*
- * @param <E> The type of elements to store in the list.
+ * @param <P> The type of {@link Property} to store in the list.
*/
-public class TableField<E> extends Field<TableField<E>> {
+public class TableField<P> extends Field<TableField<P>> {
/**
* Create a writeable list as the data model.
*/
- private final ListProperty<E> mViewProperty = new SimpleListProperty<>(
+ private final ListProperty<P> mViewProperty = new SimpleListProperty<>(
observableList( new ArrayList<>() )
);
- public static <E> TableField<E> ofListType() {
- return new TableField<>();
+ /**
+ * Contains the data model entries to persist.
+ */
+ private final ListProperty<P> mSaveProperty;
+
+ /**
+ * Creates a new {@link TableField} with a reference to the list that is to
+ * be persisted.
+ *
+ * @param persist A list of items that will be persisted.
+ * @param <P> The type of elements in the list to persist.
+ * @return A new {@link TableField} used to help render a UI widget.
+ */
+ public static <P> TableField<P> ofListType( final ListProperty<P> persist ) {
+ return new TableField<>( persist );
}
- private TableField() {
+ private TableField( final ListProperty<P> property ) {
+ mSaveProperty = property;
}
- public ListProperty<E> viewProperty() {
+ /**
+ * Returns the data model that seeds the user interface. At any point the
+ * user may cancel editing, which will revert to the previously persisted
+ * set.
+ *
+ * @return The source for values displayed in the UI.
+ */
+ public ListProperty<P> viewProperty() {
return mViewProperty;
}
+ /**
+ * Called when a new UI instance is opened.
+ *
+ * @param bindingMode Indicates how the view data model is bound to the
+ * persistence data model.
+ */
@Override
public void setBindingMode( final BindingMode bindingMode ) {
- System.out.println( "BIND TO: " + bindingMode );
+ if( CONTINUOUS.equals( bindingMode ) ) {
+ mViewProperty.get().addAll( mSaveProperty.get() );
+ }
}
}
+ /**
+ * Update the properties to save by copying the properties updated in the
+ * user interface (i.e., the view).
+ */
@Override
public void persist() {
- System.out.println( "PURSIST: " + mViewProperty );
+ mSaveProperty.get().addAll( mViewProperty.get() );
}
+ /**
+ * The {@link TableField} doesn't bind values, as such the reset can be
+ * a no-op because only {@link #persist()} will update the properties to
+ * save.
+ */
@Override
- public void reset() {
- System.out.println( "RESET" );
- }
+ public void reset() {}
}
src/main/java/com/keenwrite/preferences/Workspace.java
KEY_UI_FILES_PATH,
createSetProperty( new HashSet<String>() )
- ),
- entry(
- KEY_DOC_META,
- createSetProperty( new HashSet<Entry<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 );
- }
-
- /**
- * 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 ) );
- }
-
- private static <E> SetProperty<E> createSetProperty( final Set<E> set ) {
- return new SimpleSetProperty<>( observableSet( set ) );
- }
-
- /**
- * @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.
- */
- @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 );
- }
-
- /**
- * 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 ) );
+ )
+ );
+
+ private final Map<Key, ListProperty<?>> LISTS = Map.ofEntries(
+ entry(
+ KEY_DOC_META,
+ createListProperty( new LinkedList<Entry<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 set 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 {@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 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 List}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return An observable property to be persisted.
+ */
+ @SuppressWarnings( "unchecked" )
+ public <T> ListProperty<T> listsProperty( final Key key ) {
+ assert key != null;
+ return (ListProperty<T>) LISTS.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 ) );
+ }
+
+ private static <E> SetProperty<E> createSetProperty( final Set<E> set ) {
+ return new SimpleSetProperty<>( observableSet( set ) );
+ }
+
+ private static <E> ListProperty<E> createListProperty( final List<E> list ) {
+ return new SimpleListProperty<>( observableArrayList( list ) );
+
+ }
+
+ /**
+ * @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.
+ */
+ @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 loadListKeys( final Consumer<Key> consumer ) {
+ LISTS.keySet().forEach( consumer );
+ }
+
+ /**
+ * Calls the given consumer for all single-value keys. Sets use
+ * {@link #saveSets(BiConsumer)} and lists use {@link #saveLists(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 in the set.
+ */
+ public void saveSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
+ SETS.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 in the list.
+ */
+ public void saveLists( final BiConsumer<Key, ListProperty<?>> consumer ) {
+ LISTS.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 ) );
+ }
+ );
+
+ saveLists(
+ ( key, list ) -> {
+ final var keyName = key.toString();
+ list.forEach(
+ value -> System.out.printf( "SAVE K/V: %s = %s%n", 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 property = valuesProperty( key );
+ property.setValue( unmarshall( property, configValue ) );
+ }
+ } );
+
+ loadSetKeys( key -> {
+ final var configSet =
+ new LinkedHashSet<>( config.getList( key.toString() ) );
+ final var property = setsProperty( key );
+ property.setValue( observableSet( configSet ) );
+ } );
+
+ loadListKeys( key -> {
+ System.out.println( "LOAD LIST KEY: " + key );
+ final var configList =
+ new LinkedList<>( config.getList( key.toString() ) );
+ final var property = listsProperty( key );
+ property.setValue( observableArrayList( configList ) );
} );
} catch( final Exception ex ) {