Dave Jarvis' Repositories

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

Allow user to select theme when exporting to PDF

Author DaveJarvis <email>
Date 2021-05-08 17:19:48 GMT-0700
Commit ef0709fee5d36b9d0926ccfcee3c04a598894b57
Parent 87a48ff
src/main/java/com/keenwrite/MainPane.java
import com.panemu.tiwulfx.control.dock.DetachableTab;
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
-import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.*;
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
-import static java.awt.Desktop.Action.BROWSE;
-import static java.awt.Desktop.getDesktop;
import static java.lang.String.format;
import static java.lang.System.getProperty;
src/main/java/com/keenwrite/preferences/PreferencesController.java
Setting.of( label( KEY_TYPESET_CONTEXT_PATH ) ),
Setting.of( title( KEY_TYPESET_CONTEXT_PATH ),
- fileProperty( KEY_TYPESET_CONTEXT_PATH ), true ),
- Setting.of( label( KEY_TYPESET_CONTEXT_ENV ) ),
- Setting.of( title( KEY_TYPESET_CONTEXT_ENV ),
- stringProperty( KEY_TYPESET_CONTEXT_ENV ) )
+ fileProperty( KEY_TYPESET_CONTEXT_PATH ), true )
)
),
src/main/java/com/keenwrite/preferences/Workspace.java
*/
public final class Workspace {
- //@formatter:off
private final Map<Key, Property<?>> VALUES = Map.ofEntries(
entry( KEY_META_VERSION, asStringProperty( getVersion() ) ),
entry( KEY_UI_RECENT_DOCUMENT, asFileProperty( DOCUMENT_DEFAULT ) ),
entry( KEY_UI_RECENT_DEFINITION, asFileProperty( DEFINITION_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 ) ),
+ //@formatter:on
entry( KEY_UI_WINDOW_X, asDoubleProperty( WINDOW_X_DEFAULT ) ),
entry( KEY_TYPESET_CONTEXT_PATH, asFileProperty( USER_DIRECTORY ) ),
- entry( KEY_TYPESET_CONTEXT_ENV, asStringProperty( "" ) )
+ entry( KEY_TYPESET_CONTEXT_THEME, asStringProperty( "boschet" ) )
);
- //@formatter:on
private StringProperty asStringProperty( final String defaultValue ) {
@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 );
@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 );
*/
public boolean toBoolean( final Key key ) {
+ assert key != null;
return (Boolean) valuesProperty( key ).getValue();
}
*/
public double toDouble( final Key key ) {
+ assert key != null;
return (Double) valuesProperty( key ).getValue();
}
public File toFile( final Key key ) {
+ assert key != null;
return fileProperty( key ).get();
}
public String toString( final Key key ) {
+ assert key != null;
return stringProperty( key ).get();
}
public Tokens toTokens( final Key began, final Key ended ) {
+ assert began != null;
+ assert ended != null;
return new Tokens( stringProperty( began ), stringProperty( ended ) );
}
@SuppressWarnings( "SameParameterValue" )
public DoubleProperty doubleProperty( final Key key ) {
+ assert key != null;
return valuesProperty( key );
}
*/
public ObjectProperty<File> fileProperty( final Key key ) {
+ assert key != null;
return valuesProperty( key );
}
public ObjectProperty<String> skinProperty( final Key key ) {
+ assert key != null;
return valuesProperty( key );
}
public LocaleProperty localeProperty( final Key key ) {
+ assert key != null;
return valuesProperty( key );
}
public StringProperty stringProperty( final Key key ) {
+ assert key != null;
return valuesProperty( key );
+ }
+
+ /**
+ * Delegates setting the property for a workspace {@link Key} to the
+ * inner property instance.
+ *
+ * @param key The name of the property value to change.
+ * @param value The new property value.
+ */
+ public void setStringProperty( final Key key, final String value ) {
+ assert value != null;
+ stringProperty( key ).set( value );
}
src/main/java/com/keenwrite/preferences/WorkspaceKeys.java
public static final Key KEY_TYPESET_CONTEXT = key( KEY_TYPESET, "context" );
public static final Key KEY_TYPESET_CONTEXT_PATH = key( KEY_TYPESET_CONTEXT, "path" );
- public static final Key KEY_TYPESET_CONTEXT_ENV = key( KEY_TYPESET_CONTEXT, "environment" );
+ public static final Key KEY_TYPESET_CONTEXT_THEME = key( KEY_TYPESET_CONTEXT, "theme" );
//@formatter:on
src/main/java/com/keenwrite/processors/PdfProcessor.java
import static com.keenwrite.events.StatusEvent.clue;
import static com.keenwrite.io.MediaType.TEXT_XML;
+import static java.nio.file.Files.deleteIfExists;
import static java.nio.file.Files.writeString;
typesetter.typeset( pathInput, pathOutput );
+
+ // Smote the temporary file after typesetting the document.
+ deleteIfExists( document );
} catch( final IOException | InterruptedException ex ) {
// Typesetter runtime exceptions will pass up the call stack.
src/main/java/com/keenwrite/processors/XhtmlProcessor.java
private String getImagePath() {
- return getWorkspace().fileProperty( KEY_IMAGES_DIR ).get().toString();
+ return getWorkspace().toFile( KEY_IMAGES_DIR ).toString();
}
private String getImageOrder() {
- return getWorkspace().stringProperty( KEY_IMAGES_ORDER ).get();
+ return getWorkspace().toString( KEY_IMAGES_ORDER );
}
src/main/java/com/keenwrite/typesetting/Typesetter.java
import com.keenwrite.io.SysFile;
-import com.keenwrite.preferences.Key;
import com.keenwrite.preferences.Workspace;
import static com.keenwrite.constants.Constants.DEFAULT_DIRECTORY;
import static com.keenwrite.events.StatusEvent.clue;
-import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_ENV;
import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_PATH;
+import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_THEME;
import static java.lang.ProcessBuilder.Redirect.DISCARD;
import static java.lang.String.format;
throw new TypesetterNotFoundException( TYPESETTER.toString() );
}
- }
-
- @SuppressWarnings( "SameParameterValue" )
- private String stringProperty( final Key key ) {
- return mWorkspace.stringProperty( key ).get();
- }
-
- @SuppressWarnings( "SameParameterValue" )
- private File fileProperty( final Key key ) {
- return mWorkspace.fileProperty( key ).get();
}
mInput = input;
mOutput = output;
- mDirectory = (parentDir == null ? DEFAULT_DIRECTORY : parentDir);
+ mDirectory = parentDir == null ? DEFAULT_DIRECTORY : parentDir;
}
/**
* Initializes ConTeXt, which means creating the cache directory if it
- * doesn't already exist.
+ * doesn't already exist. The theme entry point must be named 'main.tex'.
*
* @return {@code true} if the cache directory exists.
*/
private boolean reinitialize() {
final var filename = mOutput.getFileName();
- final var paths = fileProperty( KEY_TYPESET_CONTEXT_PATH );
- final var envs = stringProperty( KEY_TYPESET_CONTEXT_ENV );
+ final var themes = mWorkspace.toFile( KEY_TYPESET_CONTEXT_PATH );
+ final var theme = mWorkspace.toString( KEY_TYPESET_CONTEXT_THEME );
final var cacheExists = !isEmpty( getCacheDir().toPath() );
mArgs.add( "--batchmode" );
mArgs.add( "--purgeall" );
- mArgs.add( "--path='" + paths + "'" );
- mArgs.add( "--environment='" + envs + "'" );
+ mArgs.add( "--path='" + Path.of( themes.toString(), theme ) + "'" );
+ mArgs.add( "--environment='main'" );
mArgs.add( "--result='" + filename + "'" );
mArgs.add( mInput.toString() );
builder.environment().put( "TEXMFCACHE", getCacheDir().toString() );
- // Without redirecting (or draining) the stderr, the command may not
+ // Without redirecting (or draining) stderr, the command may not
// terminate successfully.
builder.redirectError( DISCARD );
*
* <p>
- * Example lines written to stdout:
+ * Example lines written to standard output:
* </p>
* <pre>{@code
src/main/java/com/keenwrite/ui/actions/ApplicationActions.java
import com.keenwrite.ui.dialogs.ImageDialog;
import com.keenwrite.ui.dialogs.LinkDialog;
+import com.keenwrite.ui.dialogs.ThemePicker;
import com.keenwrite.ui.explorer.FilePicker;
import com.keenwrite.ui.explorer.FilePickerFactory;
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
import static com.keenwrite.events.StatusEvent.clue;
+import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_PATH;
+import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_THEME;
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
import static com.keenwrite.ui.explorer.FilePickerFactory.Options;
public void file‿export‿pdf() {
- if( Typesetter.canRun() ) {
+ final var workspace = getWorkspace();
+ final var themes = workspace.toFile( KEY_TYPESET_CONTEXT_PATH );
+ final var theme = workspace.stringProperty( KEY_TYPESET_CONTEXT_THEME );
+
+ if( Typesetter.canRun() && ThemePicker.choose( themes, theme ) ) {
file‿export( APPLICATION_PDF );
}
src/main/java/com/keenwrite/ui/dialogs/ThemePicker.java
+/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.ui.dialogs;
+
+import com.keenwrite.util.FileWalker;
+import com.keenwrite.util.ResourceWalker;
+import javafx.beans.property.StringProperty;
+import javafx.scene.control.ChoiceDialog;
+import javafx.scene.control.ComboBox;
+import javafx.scene.input.KeyCode;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
+import static com.keenwrite.events.StatusEvent.clue;
+import static com.keenwrite.util.FileWalker.walk;
+
+/**
+ * Responsible for allowing the user to pick from the available themes found
+ * in the system.
+ */
+public class ThemePicker extends ChoiceDialog<String> {
+ private final File mThemes;
+ private final StringProperty mTheme;
+
+ /**
+ * Construction must use static method to allow caching themes in the
+ * future, if needed.
+ *
+ * @see #choose(File, StringProperty)
+ */
+ @SuppressWarnings( "rawtypes" )
+ private ThemePicker( final File themes, final StringProperty theme ) {
+ mThemes = themes;
+ mTheme = theme;
+ setGraphic( ICON_DIALOG_NODE );
+ setTitle( get( "Dialog.theme.title" ) );
+ setHeaderText( get( "Dialog.theme.header" ) );
+
+ final var options = (ComboBox) getDialogPane().lookup( ".combo-box" );
+ options.setOnKeyPressed( ( event ) -> {
+ // When the user presses the down arrow, open the drop-down. This prevents
+ // navigating to the cancel button.
+ if( event.getCode() == KeyCode.DOWN && !options.isShowing() ) {
+ options.show();
+ event.consume();
+ }
+ } );
+ }
+
+ /**
+ * Prompts a user to select a theme, answering {@code false} if no theme
+ * was selected. The themes must be on the native file system; using the
+ * {@link FileWalker} is a little more optimal than {@link ResourceWalker}.
+ *
+ * @param themes Theme directory root.
+ * @param theme Selected theme property name.
+ * @return {@code true} if the user accepted or selected a theme.
+ */
+ public static boolean choose(
+ final File themes, final StringProperty theme ) {
+ return new ThemePicker( themes, theme ).pick();
+ }
+
+ /**
+ * @return {@code true} if the user accepted or selected a theme.
+ * @see #choose(File, StringProperty)
+ */
+ private boolean pick() {
+
+ try {
+ // Use a sorted map.
+ final var choices = new TreeMap<String, File>();
+
+ // Populate the choices with themes detected on the system.
+ walk( mThemes.toPath(), "**/theme.properties", ( path ) -> {
+ try {
+ choices.put( readThemeName( path ), path.getParent().toFile() );
+ } catch( final Exception ex ) {
+ clue( get( "Main.status.error.theme.name", path ) );
+ }
+ } );
+
+ final var items = getItems();
+ items.addAll( choices.keySet() );
+ setSelectedItem( items.get( 0 ) );
+
+ final var result = showAndWait();
+ final var themeName = result.orElseThrow();
+ final var themeDir = choices.get( themeName );
+
+ mTheme.set( themeDir.getName() );
+
+ return true;
+ } catch( final IOException ex ) {
+ clue( ex );
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the theme's human-friendly name from a file conforming to
+ * {@link Properties}.
+ *
+ * @param file A fully qualified file name readable using {@link Properties}.
+ * @return The human-friendly theme name.
+ * @throws IOException The {@link Properties} file cannot be read.
+ * @throws NullPointerException The name field is not defined.
+ */
+ private String readThemeName( final Path file ) throws Exception {
+ return read( file ).get( "name" ).toString();
+ }
+
+ private Properties read( final Path file ) throws IOException {
+ final var properties = new Properties();
+
+ try( final var in = new FileInputStream( file.toFile() ) ) {
+ properties.load( in );
+ }
+
+ return properties;
+ }
+}
src/main/java/com/keenwrite/util/FileWalker.java
+/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.util;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.function.Consumer;
+
+import static java.nio.file.FileSystems.getDefault;
+
+/**
+ * Responsible for finding files in a file system that match a particular
+ * globbing file name pattern.
+ *
+ * @see ResourceWalker#walk(String, String, Consumer)
+ */
+public class FileWalker {
+ /**
+ * Walks the given directory hierarchy for files that match the given
+ * globbing file name pattern. This will search to a depth of 10 directories
+ * deep (to avoid infinite recursion).
+ *
+ * @param path Root directory to scan for files matching the glob.
+ * @param glob Only files matching the pattern will be consumed.
+ * @param c Function to call for each matching path found.
+ * @throws IOException Could not walk the tree.
+ */
+ public static void walk(
+ final Path path, final String glob, final Consumer<Path> c )
+ throws IOException {
+ final var matcher = getDefault().getPathMatcher( "glob:" + glob );
+
+ try( final var walk = Files.walk( path, 10 ) ) {
+ for( final var it = walk.iterator(); it.hasNext(); ) {
+ final var p = it.next();
+ if( matcher.matches( p ) ) {
+ c.accept( p );
+ }
+ }
+ }
+ }
+}
src/main/java/com/keenwrite/util/ResourceWalker.java
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Consumer;
import static com.keenwrite.util.ProtocolScheme.JAR;
import static com.keenwrite.util.ProtocolScheme.valueFrom;
-import static java.nio.file.FileSystems.getDefault;
import static java.nio.file.FileSystems.newFileSystem;
import static java.util.Collections.emptyMap;
/**
- * Responsible for finding file resources.
+ * Responsible for finding file resources, regardless if they exist within
+ * a Java Archive (.jar) file or on the native file system.
+ *
+ * @see FileWalker#walk(Path, String, Consumer)
*/
public final class ResourceWalker {
/**
+ * Walks the given directory hierarchy for files that match the given
+ * globbing file name pattern.
+ *
* @param directory Root directory to scan for files matching the glob.
+ * @param glob Only files matching the pattern will be consumed.
* @param c Function to call for each matching path found.
- * @throws URISyntaxException Could not convert the resource to a URI.
* @throws IOException Could not walk the tree.
+ * @throws URISyntaxException Could not convert the resource to a URI.
*/
public static void walk(
final String directory, final String glob, final Consumer<Path> c )
throws URISyntaxException, IOException {
final var resource = ResourceWalker.class.getResource( directory );
- final var matcher = getDefault().getPathMatcher( "glob:" + glob );
if( resource != null ) {
}
- try( final var walk = Files.walk( path, 10 ) ) {
- for( final var it = walk.iterator(); it.hasNext(); ) {
- final Path p = it.next();
- if( matcher.matches( p ) ) {
- c.accept( p );
- }
- }
+ try {
+ FileWalker.walk( path, glob, c );
} finally {
if( fs != null ) { fs.close(); }
src/main/resources/com/keenwrite/messages.properties
workspace.typeset.context=ConTeXt
workspace.typeset.context.path=Paths
-workspace.typeset.context.path.desc=Comma-separated directories containing theme support files.
-workspace.typeset.context.path.title=Theme
-workspace.typeset.context.environment=Environments
-workspace.typeset.context.environment.desc=Comma-separated environment file names.
-workspace.typeset.context.environment.title=Files
+workspace.typeset.context.path.desc=Directory containing theme subdirectories.
+workspace.typeset.context.path.title=Themes
# ########################################################################
Main.status.error.undo=Cannot undo; beginning of undo history reached
Main.status.error.redo=Cannot redo; end of redo history reached
+
+Main.status.error.theme.name=Missing theme name for ''{0}''
Main.status.image.request.init=Initializing HTTP request
Dialog.link.titleLabel.text=Title (tooltip)\:
Dialog.link.urlLabel.text=Link URL\:
+
+# ########################################################################
+# Themes Dialog
+# ########################################################################
+
+Dialog.theme.title=Typesetting theme
+Dialog.theme.header=Choose a typesetting theme
# ########################################################################
Delta 247 lines added, 52 lines removed, 195-line increase