| Author | DaveJarvis <email> |
|---|---|
| Date | 2021-05-08 17:19:48 GMT-0700 |
| Commit | ef0709fee5d36b9d0926ccfcee3c04a598894b57 |
| Parent | 87a48ff |
| 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; | ||
| 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 ) | ||
| ) | ||
| ), |
| */ | ||
| 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 ); | ||
| } | ||
| 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 | ||
| 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. | ||
| 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 ); | ||
| } | ||
| 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 | ||
| 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 ); | ||
| } | ||
| +/* 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; | ||
| + } | ||
| +} | ||
| +/* 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 ); | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| +} | ||
| 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(); } | ||
| 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 |
|---|