Dave Jarvis' Repositories

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

Maintain insertion order when saving/loading files

AuthorDaveJarvis <email>
Date2020-12-25 11:58:44 GMT-0800
Commit7db79a5a7b186097d096a18641cf9084d8a23757
Parentdd0c365
README.md
### Windows
-Double-click the application to start. You will have to give the application permission to run.
+Double-click the application to start; give the application permission to run.
When upgrading to a new version, delete the following directory:
### Other
-Download and install a full version of [OpenJDK 14](https://bell-sw.com/pages/downloads/?version=java-14#mn) that includes JavaFX module support, then run:
+Download and install a full version of [OpenJDK 15](https://bell-sw.com/pages/downloads/?version=java-15#mn) that includes JavaFX module support, then run:
``` bash
README.zh-CN.md
### Other
-Download and install a full version of [OpenJDK 14](https://bell-sw.com/pages/downloads/?version=java-14#mn) that includes JavaFX module support, then run:
+Download and install a full version of [OpenJDK 15](https://bell-sw.com/pages/downloads/?version=java-15#mn) that includes JavaFX module support, then run:
``` bash
src/main/java/com/keenwrite/MainApp.java
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-import static javafx.stage.WindowEvent.WINDOW_HIDING;
/**
*/
public static void main( final String[] args ) {
- initLogging();
+ disableLogging();
launch( args );
}
/**
* Suppress logging to standard output and standard error.
*/
- private static void initLogging() {
+ private static void disableLogging() {
//LogManager.getLogManager().reset();
//System.err.close();
@Override
public void start( final Stage stage ) {
- // These must be instantiated after the UI is initialized.
+ // Must be instantiated after the UI is initialized (i.e., not in main).
mWorkspace = new Workspace();
private void initStage( final Stage stage ) {
stage.setTitle( APP_TITLE );
- stage.addEventHandler( WINDOW_HIDING, event -> save() );
stage.addEventHandler( WINDOW_CLOSE_REQUEST, event -> stop() );
stage.addEventHandler( KEY_PRESSED, event -> {
src/main/java/com/keenwrite/MainPane.java
* The order that the binned files are returned will be reflected in the
* order that the corresponding panes are rendered in the UI. Each different
- * {@link MediaType} will be created in its own pane.
+ * {@link MediaType} will be created in its own pane. Order is maintained
+ * by using {@link LinkedHashSet} instances.
* </p>
*
* @param paths The file paths to bin by {@link MediaType}.
* @return An in-order list of files, first by structured definition files,
* then by plain text documents.
*/
private List<File> bin( final SetProperty<String> paths ) {
final var map = new HashMap<MediaType, Set<File>>();
- map.put( TEXT_YAML, new HashSet<>() );
- map.put( TEXT_MARKDOWN, new HashSet<>() );
- map.put( UNDEFINED, new HashSet<>() );
+ map.put( TEXT_YAML, new LinkedHashSet<>() );
+ map.put( TEXT_MARKDOWN, new LinkedHashSet<>() );
+ map.put( UNDEFINED, new LinkedHashSet<>() );
for( final var path : paths ) {
final var file = new File( path );
final var set = map.computeIfAbsent(
- MediaType.valueOf( file ), k -> new HashSet<>()
+ MediaType.valueOf( file ), k -> new LinkedHashSet<>()
);
src/main/java/com/keenwrite/preferences/Workspace.java
import java.io.File;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Map;
-import java.util.function.BiConsumer;
-import java.util.function.BooleanSupplier;
-import java.util.function.Consumer;
-import java.util.function.Function;
-
-import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
-import static com.keenwrite.Constants.*;
-import static com.keenwrite.Launcher.getVersion;
-import static com.keenwrite.StatusBarNotifier.clue;
-import static com.keenwrite.preferences.Key.key;
-import static java.util.Map.entry;
-import static javafx.application.Platform.runLater;
-import static javafx.collections.FXCollections.observableSet;
-
-/**
- * Responsible for defining behaviours for separate projects. A workspace has
- * the ability to save and restore a session, including the window dimensions,
- * tab setup, files, and user preferences.
- * <p>
- * The configuration must support hierarchical (nested) configuration nodes
- * to persist the user interface state. Although possible with a flat
- * configuration file, it's not nearly as simple or elegant.
- * </p>
- * <p>
- * Neither JSON nor HOCON support schema validation and versioning, which makes
- * XML the more suitable configuration file format. Schema validation and
- * versioning provide future-proofing and ease of reading and upgrading previous
- * versions of the configuration file.
- * </p>
- * <p>
- * Persistent preferences may be set directly by the user or indirectly by
- * the act of using the application.
- * </p>
- * <p>
- * Note the following definitions:
- * </p>
- * <dl>
- * <dt>File</dt>
- * <dd>References a filename (no path), path, or directory.</dd>
- * <dt>Path</dt>
- * <dd>Fully qualified filename, which includes all parent directories.</dd>
- * <dt>Dir</dt>
- * <dd>Directory without a filename ({@link File#isDirectory()} is true).</dd>
- * </dl>
- */
-public class Workspace {
- private static final Key KEY_ROOT = key( "workspace" );
-
- public static final Key KEY_META = key( KEY_ROOT, "meta" );
- public static final Key KEY_META_NAME = key( KEY_META, "name" );
- public static final Key KEY_META_VERSION = key( KEY_META, "version" );
-
- public static final Key KEY_R = key( KEY_ROOT, "r" );
- public static final Key KEY_R_SCRIPT = key( KEY_R, "script" );
- public static final Key KEY_R_DIR = key( KEY_R, "dir" );
- public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" );
- public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" );
- public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" );
-
- public static final Key KEY_IMAGES = key( KEY_ROOT, "images" );
- public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" );
- public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" );
-
- public static final Key KEY_DEF = key( KEY_ROOT, "definition" );
- public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" );
- public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" );
- public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" );
- public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" );
-
- //@formatter:off
- public static final Key KEY_UI = key( KEY_ROOT, "ui" );
-
- public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" );
- public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" );
- public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT,"document" );
- public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" );
-
- public static final Key KEY_UI_FILES = key( KEY_UI, "files" );
- public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" );
-
- public static final Key KEY_UI_FONT = key( KEY_UI, "font" );
- public static final Key KEY_UI_FONT_LOCALE = key( KEY_UI_FONT, "locale" );
- public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" );
- public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" );
- public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" );
- public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" );
-
- public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" );
- public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" );
- public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" );
- public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" );
- public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" );
- public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" );
- public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" );
-
- private final Map<Key, Property<?>> VALUES = Map.ofEntries(
- entry( KEY_META_VERSION, new SimpleStringProperty( getVersion() ) ),
- entry( KEY_META_NAME, new SimpleStringProperty( "default" ) ),
-
- entry( KEY_R_SCRIPT, new SimpleStringProperty( "" ) ),
- entry( KEY_R_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
- entry( KEY_R_DELIM_BEGAN, new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
- entry( KEY_R_DELIM_ENDED, new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ) ),
-
- entry( KEY_IMAGES_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
- entry( KEY_IMAGES_ORDER, new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ) ),
-
- entry( KEY_DEF_PATH, new SimpleFileProperty( DEFINITION_DEFAULT ) ),
- entry( KEY_DEF_DELIM_BEGAN, new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
- entry( KEY_DEF_DELIM_ENDED, new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
-
- entry( KEY_UI_RECENT_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
- entry( KEY_UI_RECENT_DOCUMENT, new SimpleFileProperty( DOCUMENT_DEFAULT ) ),
- entry( KEY_UI_RECENT_DEFINITION, new SimpleFileProperty( DEFINITION_DEFAULT ) ),
-
- entry( KEY_UI_FONT_LOCALE, new SimpleLocaleProperty( LOCALE_DEFAULT ) ),
- entry( KEY_UI_FONT_EDITOR_SIZE, new SimpleDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ),
- entry( KEY_UI_FONT_PREVIEW_SIZE, new SimpleDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ),
-
- entry( KEY_UI_WINDOW_X, new SimpleDoubleProperty( WINDOW_X_DEFAULT ) ),
- entry( KEY_UI_WINDOW_Y, new SimpleDoubleProperty( WINDOW_Y_DEFAULT ) ),
- entry( KEY_UI_WINDOW_W, new SimpleDoubleProperty( WINDOW_W_DEFAULT ) ),
- entry( KEY_UI_WINDOW_H, new SimpleDoubleProperty( WINDOW_H_DEFAULT ) ),
- entry( KEY_UI_WINDOW_MAX, new SimpleBooleanProperty() ),
- entry( KEY_UI_WINDOW_FULL, new SimpleBooleanProperty() )
- );
- //@formatter:on
-
- private final Map<Key, SetProperty<?>> SETS = Map.ofEntries(
- entry( KEY_UI_FILES_PATH, new SimpleSetProperty<>() )
- );
-
- /**
- * Helps instantiate {@link Property} instances for XML configuration items.
- */
- private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
- Map.of(
- SimpleLocaleProperty.class, Locale::forLanguageTag,
- SimpleBooleanProperty.class, Boolean::parseBoolean,
- SimpleDoubleProperty.class, Double::parseDouble,
- SimpleFloatProperty.class, Float::parseFloat,
- SimpleFileProperty.class, File::new
- );
-
- public Workspace() {
- load();
- }
-
- /**
- * Returns a value that represents a setting in the application that the user
- * may configure, either directly or indirectly.
- *
- * @param key The reference to the users' preference stored in deference
- * of app reëntrance.
- * @return An observable property to be persisted.
- */
- @SuppressWarnings("unchecked")
- public <T, U extends Property<T>> U valuesProperty( final Key key ) {
- // The type that goes into the map must come out.
- return (U) VALUES.get( key );
- }
-
- /**
- * Returns a list of values that represent a setting in the application that
- * the user may configure, either directly or indirectly.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return An observable property to be persisted.
- */
- @SuppressWarnings("unchecked")
- public <T> SetProperty<T> setsProperty( final Key key ) {
- // The type that goes into the map must come out.
- return (SetProperty<T>) SETS.get( key );
- }
-
- /**
- * Returns the {@link Double} preference value associated with the given
- * {@link Key}. The caller must be sure that the given {@link Key} is
- * associated with a value that matches the return type.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public double toDouble( final Key key ) {
- return (double) valuesProperty( key ).getValue();
- }
-
- /**
- * Returns the {@link Boolean} preference value associated with the given
- * {@link Key}. The caller must be sure that the given {@link Key} is
- * associated with a value that matches the return type.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public boolean toBoolean( final Key key ) {
- return (boolean) valuesProperty( key ).getValue();
- }
-
- /**
- * Returns the {@link File} {@link Property} associated with the given
- * {@link Key} from the internal list of preference values. The caller
- * must be sure that the given {@link Key} is associated with a {@link File}
- * {@link Property}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public ObjectProperty<File> fileProperty( final Key key ) {
- return valuesProperty( key );
- }
-
- public File toFile( final Key key ) {
- return fileProperty( key ).get();
- }
-
- public StringProperty stringProperty( final Key key ) {
- return valuesProperty( key );
- }
-
- public String toString( final Key key ) {
- return stringProperty( key ).get();
- }
-
- public Tokens toTokens( final Key began, final Key ended ) {
- return new Tokens( stringProperty( began ), stringProperty( ended ) );
- }
-
- @SuppressWarnings("SameParameterValue")
- public DoubleProperty doubleProperty( final Key key ) {
- return valuesProperty( key );
- }
-
- /**
- * Calls the given consumer for all single-value keys. For lists, see
- * {@link #consumeSets(BiConsumer)}.
- *
- * @param consumer Called to accept each preference key value.
- */
- public void consumeValues( final BiConsumer<Key, Property<?>> consumer ) {
- VALUES.forEach( consumer );
- }
-
- /**
- * Calls the given consumer for all multi-value keys. For single items, see
- * {@link #consumeValues(BiConsumer)}. Callers are responsible for iterating
- * over the list of items retrieved through this method.
- *
- * @param consumer Called to accept each preference key list.
- */
- public void consumeSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
- SETS.forEach( consumer );
- }
-
- public void consumeValueKeys( final Consumer<Key> consumer ) {
- VALUES.keySet().forEach( consumer );
- }
-
- public void consumeSetKeys( final Consumer<Key> consumer ) {
- SETS.keySet().forEach( consumer );
- }
-
- /**
- * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
- * providing a value of {@code true} for the {@link BooleanSupplier} to
- * indicate the property changes always take effect.
- *
- * @param key The value to bind to the internal key property.
- * @param property The external property value that sets the internal value.
- */
- public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
- listen( key, property, () -> true );
- }
-
- /**
- * Binds a read-only property to a value in the preferences. This allows
- * user interface properties to change and the preferences will be
- * synchronized automatically.
- * <p>
- * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
- * application window states are finished before assessing whether property
- * changes should be applied. Without this, exiting the application while the
- * window is maximized would persist the window's maximum dimensions,
- * preventing restoration to its prior, non-maximum size.
- * </p>
- *
- * @param key The value to bind to the internal key property.
- * @param property The external property value that sets the internal value.
- * @param enabled Indicates whether property changes should be applied.
- */
- public <T> void listen(
- final Key key,
- final ReadOnlyProperty<T> property,
- final BooleanSupplier enabled ) {
- property.addListener(
- ( c, o, n ) ->
- runLater( () -> {
- if( enabled.getAsBoolean() ) {
- valuesProperty( key ).setValue( n );
- }
- } )
- );
- }
-
- /**
- * Saves the current workspace.
- */
- public void save() {
- try {
- final var config = new XMLConfiguration();
-
- // The root config key can only be set for an empty configuration file.
- config.setRootElementName( APP_TITLE_LOWERCASE );
-
- consumeValues( ( key, value ) -> config.setProperty(
- key.toString(), value.getValue() )
- );
-
- consumeSets(
- ( key, set ) -> {
- final String keyName = key.toString();
- set.forEach( ( value ) -> config.addProperty( keyName, value ) );
- }
- );
- new FileHandler( config ).save( FILE_PREFERENCES );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- /**
- * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file.
- * If not found, this will fall back to an empty configuration file, leaving
- * the application to fill in default values.
- */
- private void load() {
- try {
- final var config = new Configurations().xml( FILE_PREFERENCES );
-
- consumeValueKeys( ( key ) -> {
- final var configValue = config.getProperty( key.toString() );
- final var propertyValue = valuesProperty( key );
- propertyValue.setValue( unmarshall( propertyValue, configValue ) );
- } );
-
- consumeSetKeys( ( key ) -> {
- final var configSet =
- new HashSet<>( config.getList( key.toString() ) );
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
+import static com.keenwrite.Constants.*;
+import static com.keenwrite.Launcher.getVersion;
+import static com.keenwrite.StatusBarNotifier.clue;
+import static com.keenwrite.preferences.Key.key;
+import static java.util.Map.entry;
+import static javafx.application.Platform.runLater;
+import static javafx.collections.FXCollections.observableSet;
+
+/**
+ * Responsible for defining behaviours for separate projects. A workspace has
+ * the ability to save and restore a session, including the window dimensions,
+ * tab setup, files, and user preferences.
+ * <p>
+ * The configuration must support hierarchical (nested) configuration nodes
+ * to persist the user interface state. Although possible with a flat
+ * configuration file, it's not nearly as simple or elegant.
+ * </p>
+ * <p>
+ * Neither JSON nor HOCON support schema validation and versioning, which makes
+ * XML the more suitable configuration file format. Schema validation and
+ * versioning provide future-proofing and ease of reading and upgrading previous
+ * versions of the configuration file.
+ * </p>
+ * <p>
+ * Persistent preferences may be set directly by the user or indirectly by
+ * the act of using the application.
+ * </p>
+ * <p>
+ * Note the following definitions:
+ * </p>
+ * <dl>
+ * <dt>File</dt>
+ * <dd>References a filename (no path), path, or directory.</dd>
+ * <dt>Path</dt>
+ * <dd>Fully qualified filename, which includes all parent directories.</dd>
+ * <dt>Dir</dt>
+ * <dd>Directory without a filename ({@link File#isDirectory()} is true).</dd>
+ * </dl>
+ */
+public class Workspace {
+ private static final Key KEY_ROOT = key( "workspace" );
+
+ public static final Key KEY_META = key( KEY_ROOT, "meta" );
+ public static final Key KEY_META_NAME = key( KEY_META, "name" );
+ public static final Key KEY_META_VERSION = key( KEY_META, "version" );
+
+ public static final Key KEY_R = key( KEY_ROOT, "r" );
+ public static final Key KEY_R_SCRIPT = key( KEY_R, "script" );
+ public static final Key KEY_R_DIR = key( KEY_R, "dir" );
+ public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" );
+ public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" );
+ public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" );
+
+ public static final Key KEY_IMAGES = key( KEY_ROOT, "images" );
+ public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" );
+ public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" );
+
+ public static final Key KEY_DEF = key( KEY_ROOT, "definition" );
+ public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" );
+ public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" );
+ public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" );
+ public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" );
+
+ //@formatter:off
+ public static final Key KEY_UI = key( KEY_ROOT, "ui" );
+
+ public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" );
+ public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" );
+ public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT,"document" );
+ public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" );
+
+ public static final Key KEY_UI_FILES = key( KEY_UI, "files" );
+ public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" );
+
+ public static final Key KEY_UI_FONT = key( KEY_UI, "font" );
+ public static final Key KEY_UI_FONT_LOCALE = key( KEY_UI_FONT, "locale" );
+ public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" );
+ public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" );
+ public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" );
+ public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" );
+
+ public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" );
+ public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" );
+ public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" );
+ public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" );
+ public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" );
+ public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" );
+ public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" );
+
+ private final Map<Key, Property<?>> VALUES = Map.ofEntries(
+ entry( KEY_META_VERSION, new SimpleStringProperty( getVersion() ) ),
+ entry( KEY_META_NAME, new SimpleStringProperty( "default" ) ),
+
+ entry( KEY_R_SCRIPT, new SimpleStringProperty( "" ) ),
+ entry( KEY_R_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_R_DELIM_BEGAN, new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
+ entry( KEY_R_DELIM_ENDED, new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ) ),
+
+ entry( KEY_IMAGES_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_IMAGES_ORDER, new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ) ),
+
+ entry( KEY_DEF_PATH, new SimpleFileProperty( DEFINITION_DEFAULT ) ),
+ entry( KEY_DEF_DELIM_BEGAN, new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
+ entry( KEY_DEF_DELIM_ENDED, new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
+
+ entry( KEY_UI_RECENT_DIR, new SimpleFileProperty( USER_DIRECTORY ) ),
+ entry( KEY_UI_RECENT_DOCUMENT, new SimpleFileProperty( DOCUMENT_DEFAULT ) ),
+ entry( KEY_UI_RECENT_DEFINITION, new SimpleFileProperty( DEFINITION_DEFAULT ) ),
+
+ entry( KEY_UI_FONT_LOCALE, new SimpleLocaleProperty( LOCALE_DEFAULT ) ),
+ entry( KEY_UI_FONT_EDITOR_SIZE, new SimpleDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ),
+ entry( KEY_UI_FONT_PREVIEW_SIZE, new SimpleDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ),
+
+ entry( KEY_UI_WINDOW_X, new SimpleDoubleProperty( WINDOW_X_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_Y, new SimpleDoubleProperty( WINDOW_Y_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_W, new SimpleDoubleProperty( WINDOW_W_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_H, new SimpleDoubleProperty( WINDOW_H_DEFAULT ) ),
+ entry( KEY_UI_WINDOW_MAX, new SimpleBooleanProperty() ),
+ entry( KEY_UI_WINDOW_FULL, new SimpleBooleanProperty() )
+ );
+ //@formatter:on
+
+ private final Map<Key, SetProperty<?>> SETS = Map.ofEntries(
+ entry( KEY_UI_FILES_PATH, new SimpleSetProperty<>() )
+ );
+
+ /**
+z * Helps instantiate {@link Property} instances for XML configuration items.
+ */
+ private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
+ Map.of(
+ SimpleLocaleProperty.class, Locale::forLanguageTag,
+ SimpleBooleanProperty.class, Boolean::parseBoolean,
+ SimpleDoubleProperty.class, Double::parseDouble,
+ SimpleFloatProperty.class, Float::parseFloat,
+ SimpleFileProperty.class, File::new
+ );
+
+ public Workspace() {
+ load();
+ }
+
+ /**
+ * Returns a value that represents a setting in the application that the user
+ * may configure, either directly or indirectly.
+ *
+ * @param key The reference to the users' preference stored in deference
+ * of app reëntrance.
+ * @return An observable property to be persisted.
+ */
+ @SuppressWarnings("unchecked")
+ public <T, U extends Property<T>> U valuesProperty( final Key key ) {
+ // The type that goes into the map must come out.
+ return (U) VALUES.get( key );
+ }
+
+ /**
+ * Returns a list of values that represent a setting in the application that
+ * the user may configure, either directly or indirectly.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return An observable property to be persisted.
+ */
+ @SuppressWarnings("unchecked")
+ public <T> SetProperty<T> setsProperty( final Key key ) {
+ // The type that goes into the map must come out.
+ return (SetProperty<T>) SETS.get( key );
+ }
+
+ /**
+ * Returns the {@link Double} preference value associated with the given
+ * {@link Key}. The caller must be sure that the given {@link Key} is
+ * associated with a value that matches the return type.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public double toDouble( final Key key ) {
+ return (double) valuesProperty( key ).getValue();
+ }
+
+ /**
+ * Returns the {@link Boolean} preference value associated with the given
+ * {@link Key}. The caller must be sure that the given {@link Key} is
+ * associated with a value that matches the return type.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public boolean toBoolean( final Key key ) {
+ return (boolean) valuesProperty( key ).getValue();
+ }
+
+ /**
+ * Returns the {@link File} {@link Property} associated with the given
+ * {@link Key} from the internal list of preference values. The caller
+ * must be sure that the given {@link Key} is associated with a {@link File}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public ObjectProperty<File> fileProperty( final Key key ) {
+ return valuesProperty( key );
+ }
+
+ public File toFile( final Key key ) {
+ return fileProperty( key ).get();
+ }
+
+ public StringProperty stringProperty( final Key key ) {
+ return valuesProperty( key );
+ }
+
+ public String toString( final Key key ) {
+ return stringProperty( key ).get();
+ }
+
+ public Tokens toTokens( final Key began, final Key ended ) {
+ return new Tokens( stringProperty( began ), stringProperty( ended ) );
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ public DoubleProperty doubleProperty( final Key key ) {
+ return valuesProperty( key );
+ }
+
+ /**
+ * Calls the given consumer for all single-value keys. For lists, see
+ * {@link #consumeSets(BiConsumer)}.
+ *
+ * @param consumer Called to accept each preference key value.
+ */
+ public void consumeValues( final BiConsumer<Key, Property<?>> consumer ) {
+ VALUES.forEach( consumer );
+ }
+
+ /**
+ * Calls the given consumer for all multi-value keys. For single items, see
+ * {@link #consumeValues(BiConsumer)}. Callers are responsible for iterating
+ * over the list of items retrieved through this method.
+ *
+ * @param consumer Called to accept each preference key list.
+ */
+ public void consumeSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
+ SETS.forEach( consumer );
+ }
+
+ public void consumeValueKeys( final Consumer<Key> consumer ) {
+ VALUES.keySet().forEach( consumer );
+ }
+
+ public void consumeSetKeys( final Consumer<Key> consumer ) {
+ SETS.keySet().forEach( consumer );
+ }
+
+ /**
+ * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
+ * providing a value of {@code true} for the {@link BooleanSupplier} to
+ * indicate the property changes always take effect.
+ *
+ * @param key The value to bind to the internal key property.
+ * @param property The external property value that sets the internal value.
+ */
+ public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
+ listen( key, property, () -> true );
+ }
+
+ /**
+ * Binds a read-only property to a value in the preferences. This allows
+ * user interface properties to change and the preferences will be
+ * synchronized automatically.
+ * <p>
+ * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
+ * application window states are finished before assessing whether property
+ * changes should be applied. Without this, exiting the application while the
+ * window is maximized would persist the window's maximum dimensions,
+ * preventing restoration to its prior, non-maximum size.
+ * </p>
+ *
+ * @param key The value to bind to the internal key property.
+ * @param property The external property value that sets the internal value.
+ * @param enabled Indicates whether property changes should be applied.
+ */
+ public <T> void listen(
+ final Key key,
+ final ReadOnlyProperty<T> property,
+ final BooleanSupplier enabled ) {
+ property.addListener(
+ ( c, o, n ) ->
+ runLater( () -> {
+ if( enabled.getAsBoolean() ) {
+ valuesProperty( key ).setValue( n );
+ }
+ } )
+ );
+ }
+
+ /**
+ * Saves the current workspace.
+ */
+ public void save() {
+ try {
+ final var config = new XMLConfiguration();
+
+ // The root config key can only be set for an empty configuration file.
+ config.setRootElementName( APP_TITLE_LOWERCASE );
+
+ consumeValues( ( key, value ) -> config.setProperty(
+ key.toString(), value.getValue() )
+ );
+
+ consumeSets(
+ ( key, set ) -> {
+ final String keyName = key.toString();
+ set.forEach( ( value ) -> config.addProperty( keyName, value ) );
+ }
+ );
+ new FileHandler( config ).save( FILE_PREFERENCES );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ /**
+ * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file.
+ * If not found, this will fall back to an empty configuration file, leaving
+ * the application to fill in default values.
+ */
+ private void load() {
+ try {
+ final var config = new Configurations().xml( FILE_PREFERENCES );
+
+ consumeValueKeys( ( key ) -> {
+ final var configValue = config.getProperty( key.toString() );
+ final var propertyValue = valuesProperty( key );
+ propertyValue.setValue( unmarshall( propertyValue, configValue ) );
+ } );
+
+ consumeSetKeys( ( key ) -> {
+ final var configSet =
+ new LinkedHashSet<>( config.getList( key.toString() ) );
final var propertySet = setsProperty( key );
propertySet.setValue( observableSet( configSet ) );
src/main/java/com/keenwrite/ui/controls/BrowseFileButton.java
*/
public class BrowseFileButton extends Button {
- private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
+
+ private final List<ExtensionFilter> mExtensionFilters = new ArrayList<>();
+ private final ObjectProperty<Path> mBasePath = new SimpleObjectProperty<>();
+ private final ObjectProperty<String> mUrl = new SimpleObjectProperty<>();
public BrowseFileButton() {
setGraphic(
FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT )
);
setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
setOnAction( this::browse );
- disableProperty().bind( basePath.isNull() );
+ disableProperty().bind( mBasePath.isNull() );
// workaround for a JavaFX bug:
public void addExtensionFilter( ExtensionFilter extensionFilter ) {
- extensionFilters.add( extensionFilter );
- }
-
- // 'basePath' property
- private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
-
- public Path getBasePath() {
- return basePath.get();
- }
-
- public void setBasePath( Path basePath ) {
- this.basePath.set( basePath );
+ mExtensionFilters.add( extensionFilter );
}
-
- // 'url' property
- private final ObjectProperty<String> url = new SimpleObjectProperty<>();
public ObjectProperty<String> urlProperty() {
- return url;
+ return mUrl;
}
- protected void browse( ActionEvent e ) {
- FileChooser fileChooser = new FileChooser();
+ private void browse( ActionEvent e ) {
+ var fileChooser = new FileChooser();
fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
- fileChooser.getExtensionFilters().addAll( extensionFilters );
+ fileChooser.getExtensionFilters().addAll( mExtensionFilters );
fileChooser.getExtensionFilters()
.add( new ExtensionFilter( Messages.get(
"BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
fileChooser.setInitialDirectory( getInitialDirectory() );
- File result = fileChooser.showOpenDialog( getScene().getWindow() );
+ var result = fileChooser.showOpenDialog( getScene().getWindow() );
if( result != null ) {
updateUrl( result );
}
}
- protected File getInitialDirectory() {
+ private File getInitialDirectory() {
//TODO build initial directory based on current value of 'url' property
return getBasePath().toFile();
}
- protected void updateUrl( File file ) {
+ private void updateUrl( File file ) {
String newUrl;
try {
newUrl = getBasePath().relativize( file.toPath() ).toString();
} catch( IllegalArgumentException ex ) {
newUrl = file.toString();
}
- url.set( newUrl.replace( '\\', '/' ) );
+ mUrl.set( newUrl.replace( '\\', '/' ) );
+ }
+
+ public void setBasePath( Path basePath ) {
+ this.mBasePath.set( basePath );
+ }
+
+ private Path getBasePath() {
+ return mBasePath.get();
}
}
Delta385 lines added, 389 lines removed, 4-line decrease