| 79 | 79 | Typesetting to PDF files requires the following: |
| 80 | 80 | |
| 81 | * [Theme Pack](https://github.com/DaveJarvis/keenwrite-themes/releases/latest/download/theme-pack.zip) | |
| 81 | * [Theme Pack](https://gitlab.com/DaveJarvis/keenwrite-themes/-/releases/permalink/latest/downloads/theme-pack.zip) | |
| 82 | 82 | * [ConTeXt](https://wiki.contextgarden.net/Installation) |
| 83 | 83 |
| 73 | 73 | 排版到 PDF 文件需要以下內容: |
| 74 | 74 | |
| 75 | * [Theme Pack](https://github.com/DaveJarvis/keenwrite-themes/releases/latest/download/theme-pack.zip) | |
| 75 | * [Theme Pack](https://gitlab.com/DaveJarvis/keenwrite-themes/-/releases/permalink/latest/downloads/theme-pack.zip) | |
| 76 | 76 | * [ConTeXt](https://wiki.contextgarden.net/Installation) |
| 77 | 77 |
| 23 | 23 | spotbugs { |
| 24 | 24 | excludeFilter.set( |
| 25 | file( "${projectDir}/bug-filter.xml" ) | |
| 25 | file( "${projectDir}/bug-filter.xml" ) | |
| 26 | 26 | ) |
| 27 | 27 | } |
| ... | ||
| 62 | 62 | |
| 63 | 63 | def moduleSecurity = [ |
| 64 | '--add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED', | |
| 65 | '--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED', | |
| 66 | '--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED', | |
| 67 | '--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED', | |
| 68 | '--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED', | |
| 69 | '--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED', | |
| 70 | '--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', | |
| 71 | '--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED', | |
| 72 | '--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED', | |
| 73 | '--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED', | |
| 74 | '--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED', | |
| 75 | '--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED', | |
| 76 | '--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED', | |
| 64 | '--add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED', | |
| 65 | '--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED', | |
| 66 | '--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED', | |
| 67 | '--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED', | |
| 68 | '--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED', | |
| 69 | '--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED', | |
| 70 | '--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', | |
| 71 | '--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED', | |
| 72 | '--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED', | |
| 73 | '--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED', | |
| 74 | '--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED', | |
| 75 | '--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED', | |
| 76 | '--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED', | |
| 77 | 77 | ] |
| 78 | 78 | |
| ... | ||
| 91 | 91 | def v_junit = '5.10.1' |
| 92 | 92 | def v_flexmark = '0.64.8' |
| 93 | def v_jackson = '2.15.3' | |
| 93 | def v_jackson = '2.16.0' | |
| 94 | 94 | def v_echosvg = '1.0.1' |
| 95 | 95 | def v_picocli = '4.7.5' |
| 96 | 96 | |
| 97 | 97 | // JavaFX |
| 98 | 98 | implementation 'org.controlsfx:controlsfx:11.2.0' |
| 99 | 99 | implementation 'org.fxmisc.richtext:richtextfx:0.11.2' |
| 100 | 100 | implementation 'org.fxmisc.flowless:flowless:0.7.2' |
| 101 | 101 | implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3' |
| 102 | implementation 'com.miglayout:miglayout-javafx:11.2' | |
| 102 | implementation 'com.miglayout:miglayout-javafx:11.3' | |
| 103 | 103 | implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.16.0' |
| 104 | 104 | implementation 'com.panemu:tiwulfx-dock:0.2' |
| ... | ||
| 209 | 209 | compileJava { |
| 210 | 210 | options.compilerArgs += [ |
| 211 | "-Xlint:unchecked", | |
| 212 | "-Xlint:deprecation", | |
| 213 | "-Aproject=${applicationPackage}/${applicationName}" | |
| 211 | "-Xlint:unchecked", | |
| 212 | "-Xlint:deprecation", | |
| 213 | "-Aproject=${applicationPackage}/${applicationName}" | |
| 214 | 214 | ] |
| 215 | 215 | } |
| ... | ||
| 236 | 236 | from { |
| 237 | 237 | (configurations.runtimeClasspath.findAll { !it.path.endsWith( ".pom" ) }) |
| 238 | .collect { it.isDirectory() ? it : zipTree( it ) } | |
| 238 | .collect { it.isDirectory() ? it : zipTree( it ) } | |
| 239 | 239 | } |
| 240 | 240 | |
| 31 | 31 | WORKDIR $DOWNLOAD_DIR |
| 32 | 32 | |
| 33 | # Carlito (Calibri replacement) | |
| 34 | ADD "https://github.com/googlefonts/carlito/raw/main/fonts/ttf/Carlito-Regular.ttf" "Carlito-Regular.ttf" | |
| 35 | ADD "https://github.com/googlefonts/carlito/raw/main/fonts/ttf/Carlito-Bold.ttf" "Carlito-Bold.ttf" | |
| 36 | ADD "https://github.com/googlefonts/carlito/raw/main/fonts/ttf/Carlito-Italic.ttf" "Carlito-Italic.ttf" | |
| 37 | ADD "https://github.com/googlefonts/carlito/raw/main/fonts/ttf/Carlito-BoldItalic.ttf" "Carlito-BoldItalic.ttf" | |
| 38 | ||
| 39 | # Open Sans Emoji | |
| 40 | ADD "https://github.com/MorbZ/OpenSansEmoji/raw/master/OpenSansEmoji.ttf" "OpenSansEmoji.ttf" | |
| 41 | ||
| 42 | # Underwood Quiet Tab | |
| 43 | ADD "https://site.xavier.edu/polt/typewriters/Underwood_Quiet_Tab.ttf" "Underwood_Quiet_Tab.ttf" | |
| 33 | # Many fonts may be downloaded using Google's download URL. Example: | |
| 34 | # https://fonts.google.com/download?family=Roboto%20Mono | |
| 44 | 35 | |
| 45 | # Archives | |
| 46 | ADD "https://www.omnibus-type.com/wp-content/uploads/Archivo-Narrow.zip" "archivo-narrow.zip" | |
| 47 | ADD "https://fonts.google.com/download?family=Courier%20Prime" "courier-prime.zip" | |
| 48 | ADD "https://fonts.google.com/download?family=Inconsolata" "inconsolata.zip" | |
| 49 | ADD "https://fonts.google.com/download?family=Libre%20Baskerville" "libre-baskerville.zip" | |
| 50 | ADD "https://fonts.google.com/download?family=Niconne" "niconne.zip" | |
| 51 | ADD "https://fonts.google.com/download?family=Nunito" "nunito.zip" | |
| 52 | ADD "https://fonts.google.com/download?family=Roboto" "roboto.zip" | |
| 53 | ADD "https://fonts.google.com/download?family=Roboto%20Mono" "roboto-mono.zip" | |
| 54 | ADD "https://github.com/adobe-fonts/source-serif/releases/download/4.004R/source-serif-4.004.zip" "source-serif.zip" | |
| 36 | # Fonts are repacked with minimal file set, flat directory, and license. | |
| 37 | ADD "https://fonts.keenwrite.com/download/andada-pro.zip" ./ | |
| 38 | ADD "https://fonts.keenwrite.com/download/archivo-narrow.zip" ./ | |
| 39 | ADD "https://fonts.keenwrite.com/download/carlito.zip" ./ | |
| 40 | ADD "https://fonts.keenwrite.com/download/courier-prime.zip" ./ | |
| 41 | ADD "https://fonts.keenwrite.com/download/inconsolata.zip" ./ | |
| 42 | ADD "https://fonts.keenwrite.com/download/libre-baskerville.zip" ./ | |
| 43 | ADD "https://fonts.keenwrite.com/download/niconne.zip" ./ | |
| 44 | ADD "https://fonts.keenwrite.com/download/nunito.zip" ./ | |
| 45 | ADD "https://fonts.keenwrite.com/download/open-sans-emoji.zip" ./ | |
| 46 | ADD "https://fonts.keenwrite.com/download/pt-mono.zip" ./ | |
| 47 | ADD "https://fonts.keenwrite.com/download/pt-sans.zip" ./ | |
| 48 | ADD "https://fonts.keenwrite.com/download/pt-serif.zip" ./ | |
| 49 | ADD "https://fonts.keenwrite.com/download/roboto.zip" ./ | |
| 50 | ADD "https://fonts.keenwrite.com/download/roboto-mono.zip" ./ | |
| 51 | ADD "https://fonts.keenwrite.com/download/source-serif-4.zip" ./ | |
| 52 | ADD "https://fonts.keenwrite.com/download/underwood.zip" ./ | |
| 55 | 53 | |
| 56 | 54 | # Typesetting software |
| ... | ||
| 68 | 66 | add ca-certificates curl fontconfig inkscape rsync && \ |
| 69 | 67 | mkdir -p \ |
| 70 | "$FONTS_DIR" "$INSTALL_DIR" \ | |
| 71 | "$TARGET_DIR" "$SOURCE_DIR" "$THEMES_DIR" "$IMAGES_DIR" "$CACHES_DIR" && \ | |
| 68 | "$FONTS_DIR" \ | |
| 69 | "$INSTALL_DIR" \ | |
| 70 | "$TARGET_DIR" \ | |
| 71 | "$SOURCE_DIR" \ | |
| 72 | "$THEMES_DIR" \ | |
| 73 | "$IMAGES_DIR" \ | |
| 74 | "$CACHES_DIR" && \ | |
| 72 | 75 | echo "export CONTEXT_HOME=\"$CONTEXT_HOME\"" >> $PROFILE && \ |
| 73 | 76 | echo "export PATH=\"\$PATH:\$CONTEXT_HOME/tex/texmf-linuxmusl/bin\"" >> $PROFILE && \ |
| 74 | 77 | echo "export OSFONTDIR=\"/usr/share/fonts//\"" >> $PROFILE && \ |
| 75 | 78 | echo "PS1='\\u@typesetter:\\w\\$ '" >> $PROFILE && \ |
| 76 | 79 | unzip -d $CONTEXT_HOME $DOWNLOAD_DIR/context.zip && \ |
| 77 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "Archivo-Narrow/otf/*.otf" && \ | |
| 80 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/andada-pro.zip "*.otf" && \ | |
| 81 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "*.otf" && \ | |
| 82 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/carlito.zip "*.ttf" && \ | |
| 78 | 83 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/courier-prime.zip "*.ttf" && \ |
| 79 | 84 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/libre-baskerville.zip "*.ttf" && \ |
| 80 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/inconsolata.zip "**/Inconsolata/*.ttf" && \ | |
| 85 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/inconsolata.zip "*.ttf" && \ | |
| 81 | 86 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/niconne.zip "*.ttf" && \ |
| 82 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/nunito.zip "static/*.ttf" && \ | |
| 87 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/nunito.zip "*.ttf" && \ | |
| 88 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/open-sans-emoji.zip "*.ttf" && \ | |
| 89 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/pt-mono.zip "*.ttf" && \ | |
| 90 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/pt-sans.zip "*.ttf" && \ | |
| 91 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/pt-serif.zip "*.ttf" && \ | |
| 83 | 92 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto.zip "*.ttf" && \ |
| 84 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto-mono.zip "static/*.ttf" && \ | |
| 85 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/source-serif.zip "source-serif-4.004/OTF/SourceSerif4-*.otf" && \ | |
| 86 | mv $DOWNLOAD_DIR/*tf $FONTS_DIR && \ | |
| 93 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto-mono.zip "*.ttf" && \ | |
| 94 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/source-serif-4.zip "*.otf" && \ | |
| 95 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/underwood.zip "*.ttf" && \ | |
| 87 | 96 | fc-cache -f -v && \ |
| 88 | 97 | mkdir -p tex && \ |
| 89 | #rsync \ | |
| 90 | #--recursive --links --times \ | |
| 91 | #--info=progress2,remove,symsafe,flist,del \ | |
| 92 | #--human-readable --del \ | |
| 93 | #rsync://contextgarden.net/minimals/current/modules/ modules && \ | |
| 94 | #rsync \ | |
| 95 | #-rlt --exclude=/VERSION --del modules/*/ tex/texmf-modules && \ | |
| 96 | 98 | sh install.sh && \ |
| 97 | rm -f $DOWNLOAD_DIR/*.zip && \ | |
| 98 | 99 | rm -rf \ |
| 99 | 100 | "modules" \ |
| 100 | 101 | "/var/cache" \ |
| 101 | 102 | "/usr/share/icons" \ |
| 103 | "/opt/context/tex/texmf-context/source" \ | |
| 104 | $DOWNLOAD_DIR/*.zip \ | |
| 102 | 105 | $CONTEXT_HOME/tex/texmf-modules/doc \ |
| 103 | 106 | $CONTEXT_HOME/tex/texmf-context/doc && \ |
| 2 | 2 | |
| 3 | 3 | This document describes how to maintain the containerized typesetting system. |
| 4 | Broadly, the container is built locally then deployed to a web server capable | |
| 5 | of serving static web pages. | |
| 4 | The container is built locally then deployed to a web server capable of | |
| 5 | serving static web pages. | |
| 6 | 6 | |
| 7 | 7 | ## Installation wizard |
| 8 | 8 | |
| 9 | 9 | The installation wizard is responsible for installing the containerization |
| 10 | 10 | software and the container image. The container manager class loads the |
| 11 | 11 | image from a URL. That URL is defined in the `messages.properties` file. |
| 12 | 12 | |
| 13 | 13 | # Upgrade |
| 14 | 14 | |
| 15 | Upgrade the containerization software as follows: | |
| 15 | Upgrade the containerization software (e.g., podman or docker) as follows: | |
| 16 | ||
| 17 | 1. Download the latest container version. | |
| 18 | ||
| 19 | wget -q $(\ | |
| 20 | wget \ | |
| 21 | -q -O- \ | |
| 22 | https://api.github.com/repos/containers/podman/releases/latest | \ | |
| 23 | jq \ | |
| 24 | -r '.assets[] | select(.name | contains("exe")) | .browser_download_url') | |
| 25 | ||
| 26 | 1. Compute the SHA: | |
| 27 | ||
| 28 | sha256sum *exe | cut -f1 -d' ' | |
| 16 | 29 | |
| 17 | 30 | 1. Edit `src/main/resources/com/keenwrite/messages.properties`. |
| 18 | 31 | 1. Set `Wizard.typesetter.container.version` to the latest version. |
| 19 | 32 | 1. Set `Wizard.typesetter.container.checksum` to the Windows version checksum. |
| 20 | 1. Set `Wizard.typesetter.container.image.version` to the latest image version. | |
| 33 | 1. Set `Wizard.typesetter.container.image.version` to the new image version. | |
| 21 | 34 | 1. Save the file. |
| 22 | 35 | |
| 23 | The containerization software versions are changed. | |
| 36 | The containerization software version is changed. | |
| 24 | 37 | |
| 25 | 38 | # Publish |
| 26 | 39 | |
| 27 | 40 | Publish the changes to the container image as follows: |
| 28 | 41 | |
| 29 | 42 | ``` bash |
| 30 | ./manage.sh --build | |
| 31 | ./manage.sh --export | |
| 32 | ./manage.sh --publish | |
| 43 | ./manage.sh --delete --build --export --publish | |
| 33 | 44 | ``` |
| 34 | 45 |
| 91 | 91 | # --------------------------------------------------------------------------- |
| 92 | 92 | utile_build() { |
| 93 | $log "Building" | |
| 93 | $log "Building container version ${CONTAINER_VERSION}" | |
| 94 | 94 | |
| 95 | 95 | # Show what commands are run while building, but not the commands' output. |
| 68 | 68 | * `[@type-name:label]` (reference) |
| 69 | 69 | |
| 70 | The `type-name` can be any alphanumeric value, starting with a letter. | |
| 71 | Type names are user-defined categories for the item type. Labels are | |
| 72 | user-defined identifiers that must be unique per item. | |
| 70 | The `type-name` can be any alphanumeric value, starting with a letter or | |
| 71 | ideogram. Type names are user-defined categories for the item type. Labels | |
| 72 | are user-defined identifiers that must be unique per item. | |
| 73 | 73 | |
| 74 | 74 | Consider the following example: |
| 2 | 2 | package com.keenwrite; |
| 3 | 3 | |
| 4 | import com.keenwrite.editors.TextDefinition; | |
| 5 | import com.keenwrite.editors.TextEditor; | |
| 6 | import com.keenwrite.editors.TextResource; | |
| 7 | import com.keenwrite.editors.common.ScrollEventHandler; | |
| 8 | import com.keenwrite.editors.common.VariableNameInjector; | |
| 9 | import com.keenwrite.editors.definition.DefinitionEditor; | |
| 10 | import com.keenwrite.editors.definition.TreeTransformer; | |
| 11 | import com.keenwrite.editors.definition.yaml.YamlTreeTransformer; | |
| 12 | import com.keenwrite.editors.markdown.MarkdownEditor; | |
| 13 | import com.keenwrite.events.*; | |
| 14 | import com.keenwrite.events.spelling.LexiconLoadedEvent; | |
| 15 | import com.keenwrite.io.MediaType; | |
| 16 | import com.keenwrite.io.MediaTypeExtension; | |
| 17 | import com.keenwrite.preferences.Workspace; | |
| 18 | import com.keenwrite.preview.HtmlPreview; | |
| 19 | import com.keenwrite.processors.HtmlPreviewProcessor; | |
| 20 | import com.keenwrite.processors.Processor; | |
| 21 | import com.keenwrite.processors.ProcessorContext; | |
| 22 | import com.keenwrite.processors.ProcessorFactory; | |
| 23 | import com.keenwrite.processors.r.Engine; | |
| 24 | import com.keenwrite.processors.r.RBootstrapController; | |
| 25 | import com.keenwrite.service.events.Notifier; | |
| 26 | import com.keenwrite.spelling.api.SpellChecker; | |
| 27 | import com.keenwrite.spelling.impl.PermissiveSpeller; | |
| 28 | import com.keenwrite.spelling.impl.SymSpellSpeller; | |
| 29 | import com.keenwrite.typesetting.installer.TypesetterInstaller; | |
| 30 | import com.keenwrite.ui.explorer.FilePickerFactory; | |
| 31 | import com.keenwrite.ui.heuristics.DocumentStatistics; | |
| 32 | import com.keenwrite.ui.outline.DocumentOutline; | |
| 33 | import com.keenwrite.ui.spelling.TextEditorSpellChecker; | |
| 34 | import com.keenwrite.util.GenericBuilder; | |
| 35 | import com.panemu.tiwulfx.control.dock.DetachableTab; | |
| 36 | import com.panemu.tiwulfx.control.dock.DetachableTabPane; | |
| 37 | import javafx.beans.property.*; | |
| 38 | import javafx.collections.ListChangeListener; | |
| 39 | import javafx.concurrent.Task; | |
| 40 | import javafx.event.ActionEvent; | |
| 41 | import javafx.event.Event; | |
| 42 | import javafx.event.EventHandler; | |
| 43 | import javafx.scene.Node; | |
| 44 | import javafx.scene.Scene; | |
| 45 | import javafx.scene.control.SplitPane; | |
| 46 | import javafx.scene.control.Tab; | |
| 47 | import javafx.scene.control.TabPane; | |
| 48 | import javafx.scene.control.Tooltip; | |
| 49 | import javafx.scene.control.TreeItem.TreeModificationEvent; | |
| 50 | import javafx.scene.input.KeyEvent; | |
| 51 | import javafx.stage.Stage; | |
| 52 | import javafx.stage.Window; | |
| 53 | import org.greenrobot.eventbus.Subscribe; | |
| 54 | ||
| 55 | import java.io.File; | |
| 56 | import java.io.FileNotFoundException; | |
| 57 | import java.nio.file.Path; | |
| 58 | import java.util.*; | |
| 59 | import java.util.concurrent.ExecutorService; | |
| 60 | import java.util.concurrent.ScheduledExecutorService; | |
| 61 | import java.util.concurrent.ScheduledFuture; | |
| 62 | import java.util.concurrent.atomic.AtomicBoolean; | |
| 63 | import java.util.concurrent.atomic.AtomicReference; | |
| 64 | import java.util.function.Consumer; | |
| 65 | import java.util.function.Function; | |
| 66 | import java.util.stream.Collectors; | |
| 67 | ||
| 68 | import static com.keenwrite.ExportFormat.NONE; | |
| 69 | import static com.keenwrite.Launcher.terminate; | |
| 70 | import static com.keenwrite.Messages.get; | |
| 71 | import static com.keenwrite.constants.Constants.*; | |
| 72 | import static com.keenwrite.events.Bus.register; | |
| 73 | import static com.keenwrite.events.StatusEvent.clue; | |
| 74 | import static com.keenwrite.io.MediaType.*; | |
| 75 | import static com.keenwrite.io.MediaType.TypeName.TEXT; | |
| 76 | import static com.keenwrite.io.SysFile.toFile; | |
| 77 | import static com.keenwrite.preferences.AppKeys.*; | |
| 78 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 79 | import static com.keenwrite.processors.ProcessorContext.Mutator; | |
| 80 | import static com.keenwrite.processors.ProcessorContext.builder; | |
| 81 | import static com.keenwrite.processors.ProcessorFactory.createProcessors; | |
| 82 | import static java.awt.Desktop.getDesktop; | |
| 83 | import static java.util.concurrent.Executors.newFixedThreadPool; | |
| 84 | import static java.util.concurrent.Executors.newScheduledThreadPool; | |
| 85 | import static java.util.concurrent.TimeUnit.SECONDS; | |
| 86 | import static java.util.stream.Collectors.groupingBy; | |
| 87 | import static javafx.application.Platform.exit; | |
| 88 | import static javafx.application.Platform.runLater; | |
| 89 | import static javafx.scene.control.ButtonType.NO; | |
| 90 | import static javafx.scene.control.ButtonType.YES; | |
| 91 | import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS; | |
| 92 | import static javafx.scene.input.KeyCode.ENTER; | |
| 93 | import static javafx.scene.input.KeyCode.SPACE; | |
| 94 | import static javafx.scene.input.KeyCombination.ALT_DOWN; | |
| 95 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 96 | import static javafx.util.Duration.millis; | |
| 97 | import static javax.swing.SwingUtilities.invokeLater; | |
| 98 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 99 | ||
| 100 | /** | |
| 101 | * Responsible for wiring together the main application components for a | |
| 102 | * particular {@link Workspace} (project). These include the definition views, | |
| 103 | * text editors, and preview pane along with any corresponding controllers. | |
| 104 | */ | |
| 105 | public final class MainPane extends SplitPane { | |
| 106 | ||
| 107 | private static final ExecutorService sExecutor = newFixedThreadPool( 1 ); | |
| 108 | private static final Notifier sNotifier = Services.load( Notifier.class ); | |
| 109 | ||
| 110 | /** | |
| 111 | * Used when opening files to determine how each file should be binned and | |
| 112 | * therefore what tab pane to be opened within. | |
| 113 | */ | |
| 114 | private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of( | |
| 115 | TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED | |
| 116 | ); | |
| 117 | ||
| 118 | private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 ); | |
| 119 | private final AtomicReference<ScheduledFuture<?>> mSaveTask = | |
| 120 | new AtomicReference<>(); | |
| 121 | ||
| 122 | /** | |
| 123 | * Prevents re-instantiation of processing classes. | |
| 124 | */ | |
| 125 | private final Map<TextResource, Processor<String>> mProcessors = | |
| 126 | new HashMap<>(); | |
| 127 | ||
| 128 | private final Workspace mWorkspace; | |
| 129 | ||
| 130 | /** | |
| 131 | * Groups similar file type tabs together. | |
| 132 | */ | |
| 133 | private final List<TabPane> mTabPanes = new ArrayList<>(); | |
| 134 | ||
| 135 | /** | |
| 136 | * Renders the actively selected plain text editor tab. | |
| 137 | */ | |
| 138 | private final HtmlPreview mPreview; | |
| 139 | ||
| 140 | /** | |
| 141 | * Provides an interactive document outline. | |
| 142 | */ | |
| 143 | private final DocumentOutline mOutline = new DocumentOutline(); | |
| 144 | ||
| 145 | /** | |
| 146 | * Changing the active editor fires the value changed event. This allows | |
| 147 | * refreshes to happen when external definitions are modified and need to | |
| 148 | * trigger the processing chain. | |
| 149 | */ | |
| 150 | private final ObjectProperty<TextEditor> mTextEditor = | |
| 151 | new SimpleObjectProperty<>(); | |
| 152 | ||
| 153 | /** | |
| 154 | * Changing the active definition editor fires the value changed event. This | |
| 155 | * allows refreshes to happen when external definitions are modified and need | |
| 156 | * to trigger the processing chain. | |
| 157 | */ | |
| 158 | private final ObjectProperty<TextDefinition> mDefinitionEditor = | |
| 159 | new SimpleObjectProperty<>(); | |
| 160 | ||
| 161 | private final ObjectProperty<SpellChecker> mSpellChecker; | |
| 162 | ||
| 163 | private final TextEditorSpellChecker mEditorSpeller; | |
| 164 | ||
| 165 | /** | |
| 166 | * Called when the definition data is changed. | |
| 167 | */ | |
| 168 | private final EventHandler<TreeModificationEvent<Event>> mTreeHandler = | |
| 169 | event -> { | |
| 170 | process( getTextEditor() ); | |
| 171 | save( getTextDefinition() ); | |
| 172 | }; | |
| 173 | ||
| 174 | /** | |
| 175 | * Tracks the number of detached tab panels opened into their own windows, | |
| 176 | * which allows unique identification of subordinate windows by their title. | |
| 177 | * It is doubtful more than 128 windows, much less 256, will be created. | |
| 178 | */ | |
| 179 | private byte mWindowCount; | |
| 180 | ||
| 181 | private final VariableNameInjector mVariableNameInjector; | |
| 182 | ||
| 183 | private final RBootstrapController mRBootstrapController; | |
| 184 | ||
| 185 | private final DocumentStatistics mStatistics; | |
| 186 | ||
| 187 | @SuppressWarnings( {"FieldCanBeLocal", "unused"} ) | |
| 188 | private final TypesetterInstaller mInstallWizard; | |
| 189 | ||
| 190 | /** | |
| 191 | * Adds all content panels to the main user interface. This will load the | |
| 192 | * configuration settings from the workspace to reproduce the settings from | |
| 193 | * a previous session. | |
| 194 | */ | |
| 195 | public MainPane( final Workspace workspace ) { | |
| 196 | mWorkspace = workspace; | |
| 197 | mSpellChecker = createSpellChecker(); | |
| 198 | mEditorSpeller = createTextEditorSpellChecker( mSpellChecker ); | |
| 199 | mPreview = new HtmlPreview( workspace ); | |
| 200 | mStatistics = new DocumentStatistics( workspace ); | |
| 201 | ||
| 202 | mTextEditor.addListener( ( c, o, n ) -> { | |
| 203 | if( o != null ) { | |
| 204 | removeProcessor( o ); | |
| 205 | } | |
| 206 | ||
| 207 | if( n != null ) { | |
| 208 | mPreview.setBaseUri( n.getPath() ); | |
| 209 | updateProcessors( n ); | |
| 210 | process( n ); | |
| 211 | } | |
| 212 | } ); | |
| 213 | ||
| 214 | mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) ); | |
| 215 | mDefinitionEditor.set( createDefinitionEditor( workspace ) ); | |
| 216 | mVariableNameInjector = new VariableNameInjector( workspace ); | |
| 217 | mRBootstrapController = new RBootstrapController( | |
| 218 | workspace, mDefinitionEditor.get()::getDefinitions | |
| 219 | ); | |
| 220 | ||
| 221 | // If the user modifies the definitions, re-process the variables. | |
| 222 | mDefinitionEditor.addListener( ( c, o, n ) -> { | |
| 223 | final var textEditor = getTextEditor(); | |
| 224 | ||
| 225 | if( textEditor.isMediaType( TEXT_R_MARKDOWN ) ) { | |
| 226 | mRBootstrapController.update(); | |
| 227 | } | |
| 228 | ||
| 229 | process( textEditor ); | |
| 230 | } ); | |
| 231 | ||
| 232 | open( collect( getRecentFiles() ) ); | |
| 233 | viewPreview(); | |
| 234 | setDividerPositions( calculateDividerPositions() ); | |
| 235 | ||
| 236 | // Once the main scene's window regains focus, update the active definition | |
| 237 | // editor to the currently selected tab. | |
| 238 | runLater( () -> getWindow().setOnCloseRequest( event -> { | |
| 239 | // Order matters: Open file names must be persisted before closing all. | |
| 240 | mWorkspace.save(); | |
| 241 | ||
| 242 | if( closeAll() ) { | |
| 243 | exit(); | |
| 244 | terminate( 0 ); | |
| 245 | } | |
| 246 | ||
| 247 | event.consume(); | |
| 248 | } ) ); | |
| 249 | ||
| 250 | register( this ); | |
| 251 | initAutosave( workspace ); | |
| 252 | ||
| 253 | restoreSession(); | |
| 254 | runLater( this::restoreFocus ); | |
| 255 | ||
| 256 | mInstallWizard = new TypesetterInstaller( workspace ); | |
| 257 | } | |
| 258 | ||
| 259 | /** | |
| 260 | * Called when spellchecking can be run. This will reload the dictionary | |
| 261 | * into memory once, and then re-use it for all the existing text editors. | |
| 262 | * | |
| 263 | * @param event The event to process, having a populated word-frequency map. | |
| 264 | */ | |
| 265 | @Subscribe | |
| 266 | public void handle( final LexiconLoadedEvent event ) { | |
| 267 | final var lexicon = event.getLexicon(); | |
| 268 | ||
| 269 | try { | |
| 270 | final var checker = SymSpellSpeller.forLexicon( lexicon ); | |
| 271 | mSpellChecker.set( checker ); | |
| 272 | } catch( final Exception ex ) { | |
| 273 | clue( ex ); | |
| 274 | } | |
| 275 | } | |
| 276 | ||
| 277 | @Subscribe | |
| 278 | public void handle( final TextEditorFocusEvent event ) { | |
| 279 | mTextEditor.set( event.get() ); | |
| 280 | } | |
| 281 | ||
| 282 | @Subscribe | |
| 283 | public void handle( final TextDefinitionFocusEvent event ) { | |
| 284 | mDefinitionEditor.set( event.get() ); | |
| 285 | } | |
| 286 | ||
| 287 | /** | |
| 288 | * Typically called when a file name is clicked in the preview panel. | |
| 289 | * | |
| 290 | * @param event The event to process, must contain a valid file reference. | |
| 291 | */ | |
| 292 | @Subscribe | |
| 293 | public void handle( final FileOpenEvent event ) { | |
| 294 | final File eventFile; | |
| 295 | final var eventUri = event.getUri(); | |
| 296 | ||
| 297 | if( eventUri.isAbsolute() ) { | |
| 298 | eventFile = new File( eventUri.getPath() ); | |
| 299 | } | |
| 300 | else { | |
| 301 | final var activeFile = getTextEditor().getFile(); | |
| 302 | final var parent = activeFile.getParentFile(); | |
| 303 | ||
| 304 | if( parent == null ) { | |
| 305 | clue( new FileNotFoundException( eventUri.getPath() ) ); | |
| 306 | return; | |
| 307 | } | |
| 308 | else { | |
| 309 | final var parentPath = parent.getAbsolutePath(); | |
| 310 | eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) ); | |
| 311 | } | |
| 312 | } | |
| 313 | ||
| 314 | final var mediaType = MediaTypeExtension.fromFile( eventFile ); | |
| 315 | ||
| 316 | runLater( () -> { | |
| 317 | // Open text files locally. | |
| 318 | if( mediaType.isType( TEXT ) ) { | |
| 319 | open( eventFile ); | |
| 320 | } | |
| 321 | else { | |
| 322 | try { | |
| 323 | // Delegate opening all other file types to the operating system. | |
| 324 | getDesktop().open( eventFile ); | |
| 325 | } catch( final Exception ex ) { | |
| 326 | clue( ex ); | |
| 327 | } | |
| 328 | } | |
| 329 | } ); | |
| 330 | } | |
| 331 | ||
| 332 | @Subscribe | |
| 333 | public void handle( final CaretNavigationEvent event ) { | |
| 334 | runLater( () -> { | |
| 335 | final var textArea = getTextEditor(); | |
| 336 | textArea.moveTo( event.getOffset() ); | |
| 337 | textArea.requestFocus(); | |
| 338 | } ); | |
| 339 | } | |
| 340 | ||
| 341 | @Subscribe | |
| 342 | public void handle( final InsertDefinitionEvent<String> event ) { | |
| 343 | final var leaf = event.getLeaf(); | |
| 344 | final var editor = mTextEditor.get(); | |
| 345 | ||
| 346 | mVariableNameInjector.insert( editor, leaf ); | |
| 347 | } | |
| 348 | ||
| 349 | private void initAutosave( final Workspace workspace ) { | |
| 350 | final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE ); | |
| 351 | ||
| 352 | rate.addListener( | |
| 353 | ( c, o, n ) -> { | |
| 354 | final var taskRef = mSaveTask.get(); | |
| 355 | ||
| 356 | // Prevent multiple autosaves from running. | |
| 357 | if( taskRef != null ) { | |
| 358 | taskRef.cancel( false ); | |
| 359 | } | |
| 360 | ||
| 361 | initAutosave( rate ); | |
| 362 | } | |
| 363 | ); | |
| 364 | ||
| 365 | // Start the save listener (avoids duplicating some code). | |
| 366 | initAutosave( rate ); | |
| 367 | } | |
| 368 | ||
| 369 | private void initAutosave( final IntegerProperty rate ) { | |
| 370 | mSaveTask.set( | |
| 371 | mSaver.scheduleAtFixedRate( | |
| 372 | () -> { | |
| 373 | if( getTextEditor().isModified() ) { | |
| 374 | // Ensure the modified indicator is cleared by running on EDT. | |
| 375 | runLater( this::save ); | |
| 376 | } | |
| 377 | }, 0, rate.intValue(), SECONDS | |
| 378 | ) | |
| 379 | ); | |
| 380 | } | |
| 381 | ||
| 382 | /** | |
| 383 | * TODO: Load divider positions from exported settings, see | |
| 384 | * {@link #collect(SetProperty)} comment. | |
| 385 | */ | |
| 386 | private double[] calculateDividerPositions() { | |
| 387 | final var ratio = 100f / getItems().size() / 100; | |
| 388 | final var positions = getDividerPositions(); | |
| 389 | ||
| 390 | for( int i = 0; i < positions.length; i++ ) { | |
| 391 | positions[ i ] = ratio * i; | |
| 392 | } | |
| 393 | ||
| 394 | return positions; | |
| 395 | } | |
| 396 | ||
| 397 | /** | |
| 398 | * Opens all the files into the application, provided the paths are unique. | |
| 399 | * This may only be called for any type of files that a user can edit | |
| 400 | * (i.e., update and persist), such as definitions and text files. | |
| 401 | * | |
| 402 | * @param files The list of files to open. | |
| 403 | */ | |
| 404 | public void open( final List<File> files ) { | |
| 405 | files.forEach( this::open ); | |
| 406 | } | |
| 407 | ||
| 408 | /** | |
| 409 | * This opens the given file. Since the preview pane is not a file that | |
| 410 | * can be opened, it is safe to add a listener to the detachable pane. | |
| 411 | * This will exit early if the given file is not a regular file (i.e., a | |
| 412 | * directory). | |
| 413 | * | |
| 414 | * @param inputFile The file to open. | |
| 415 | */ | |
| 416 | private void open( final File inputFile ) { | |
| 417 | // Prevent opening directories (a non-existent "untitled.md" is fine). | |
| 418 | if( !inputFile.isFile() && inputFile.exists() ) { | |
| 419 | return; | |
| 420 | } | |
| 421 | ||
| 422 | final var mediaType = fromFilename( inputFile ); | |
| 423 | ||
| 424 | // Only allow opening text files. | |
| 425 | if( !mediaType.isType( TEXT ) ) { | |
| 426 | return; | |
| 427 | } | |
| 428 | ||
| 429 | final var tab = createTab( inputFile ); | |
| 430 | final var node = tab.getContent(); | |
| 431 | final var tabPane = obtainTabPane( mediaType ); | |
| 432 | ||
| 433 | tab.setTooltip( createTooltip( inputFile ) ); | |
| 434 | tabPane.setFocusTraversable( false ); | |
| 435 | tabPane.setTabClosingPolicy( ALL_TABS ); | |
| 436 | tabPane.getTabs().add( tab ); | |
| 437 | ||
| 438 | // Attach the tab scene factory for new tab panes. | |
| 439 | if( !getItems().contains( tabPane ) ) { | |
| 440 | addTabPane( | |
| 441 | node instanceof TextDefinition ? 0 : getItems().size(), tabPane | |
| 442 | ); | |
| 443 | } | |
| 444 | ||
| 445 | if( inputFile.isFile() ) { | |
| 446 | getRecentFiles().add( inputFile.getAbsolutePath() ); | |
| 447 | } | |
| 448 | } | |
| 449 | ||
| 450 | /** | |
| 451 | * Gives focus to the most recently edited document and attempts to move | |
| 452 | * the caret to the most recently known offset into said document. | |
| 453 | */ | |
| 454 | private void restoreSession() { | |
| 455 | final var workspace = getWorkspace(); | |
| 456 | final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT ); | |
| 457 | final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET ); | |
| 458 | ||
| 459 | for( final var pane : mTabPanes ) { | |
| 460 | for( final var tab : pane.getTabs() ) { | |
| 461 | final var tooltip = tab.getTooltip(); | |
| 462 | ||
| 463 | if( tooltip != null ) { | |
| 464 | final var tabName = tooltip.getText(); | |
| 465 | final var fileName = file.get().toString(); | |
| 466 | ||
| 467 | if( tabName.equalsIgnoreCase( fileName ) ) { | |
| 468 | final var node = tab.getContent(); | |
| 469 | ||
| 470 | pane.getSelectionModel().select( tab ); | |
| 471 | node.requestFocus(); | |
| 472 | ||
| 473 | if( node instanceof TextEditor editor ) { | |
| 474 | runLater( () -> editor.moveTo( offset.getValue() ) ); | |
| 475 | } | |
| 476 | ||
| 477 | break; | |
| 478 | } | |
| 479 | } | |
| 480 | } | |
| 481 | } | |
| 482 | } | |
| 483 | ||
| 484 | /** | |
| 485 | * Sets the focus to the middle pane, which contains the text editor tabs. | |
| 486 | */ | |
| 487 | private void restoreFocus() { | |
| 488 | // Work around a bug where focusing directly on the middle pane results | |
| 489 | // in the R engine not loading variables properly. | |
| 490 | mTabPanes.get( 0 ).requestFocus(); | |
| 491 | ||
| 492 | // This is the only line that should be required. | |
| 493 | mTabPanes.get( 1 ).requestFocus(); | |
| 494 | } | |
| 495 | ||
| 496 | /** | |
| 497 | * Opens a new text editor document using the default document file name. | |
| 498 | */ | |
| 499 | public void newTextEditor() { | |
| 500 | open( DOCUMENT_DEFAULT ); | |
| 4 | import com.keenwrite.constants.Constants; | |
| 5 | import com.keenwrite.editors.TextDefinition; | |
| 6 | import com.keenwrite.editors.TextEditor; | |
| 7 | import com.keenwrite.editors.TextResource; | |
| 8 | import com.keenwrite.editors.common.ScrollEventHandler; | |
| 9 | import com.keenwrite.editors.common.VariableNameInjector; | |
| 10 | import com.keenwrite.editors.definition.DefinitionEditor; | |
| 11 | import com.keenwrite.editors.definition.TreeTransformer; | |
| 12 | import com.keenwrite.editors.definition.yaml.YamlTreeTransformer; | |
| 13 | import com.keenwrite.editors.markdown.MarkdownEditor; | |
| 14 | import com.keenwrite.events.*; | |
| 15 | import com.keenwrite.events.spelling.LexiconLoadedEvent; | |
| 16 | import com.keenwrite.io.MediaType; | |
| 17 | import com.keenwrite.io.MediaTypeExtension; | |
| 18 | import com.keenwrite.preferences.Workspace; | |
| 19 | import com.keenwrite.preview.HtmlPreview; | |
| 20 | import com.keenwrite.processors.HtmlPreviewProcessor; | |
| 21 | import com.keenwrite.processors.Processor; | |
| 22 | import com.keenwrite.processors.ProcessorContext; | |
| 23 | import com.keenwrite.processors.ProcessorFactory; | |
| 24 | import com.keenwrite.processors.r.Engine; | |
| 25 | import com.keenwrite.processors.r.RBootstrapController; | |
| 26 | import com.keenwrite.service.events.Notifier; | |
| 27 | import com.keenwrite.spelling.api.SpellChecker; | |
| 28 | import com.keenwrite.spelling.impl.PermissiveSpeller; | |
| 29 | import com.keenwrite.spelling.impl.SymSpellSpeller; | |
| 30 | import com.keenwrite.typesetting.installer.TypesetterInstaller; | |
| 31 | import com.keenwrite.ui.explorer.FilePickerFactory; | |
| 32 | import com.keenwrite.ui.heuristics.DocumentStatistics; | |
| 33 | import com.keenwrite.ui.outline.DocumentOutline; | |
| 34 | import com.keenwrite.ui.spelling.TextEditorSpellChecker; | |
| 35 | import com.keenwrite.util.GenericBuilder; | |
| 36 | import com.panemu.tiwulfx.control.dock.DetachableTab; | |
| 37 | import com.panemu.tiwulfx.control.dock.DetachableTabPane; | |
| 38 | import javafx.beans.property.*; | |
| 39 | import javafx.collections.ListChangeListener; | |
| 40 | import javafx.concurrent.Task; | |
| 41 | import javafx.event.ActionEvent; | |
| 42 | import javafx.event.Event; | |
| 43 | import javafx.event.EventHandler; | |
| 44 | import javafx.scene.Node; | |
| 45 | import javafx.scene.Scene; | |
| 46 | import javafx.scene.control.SplitPane; | |
| 47 | import javafx.scene.control.Tab; | |
| 48 | import javafx.scene.control.TabPane; | |
| 49 | import javafx.scene.control.Tooltip; | |
| 50 | import javafx.scene.control.TreeItem.TreeModificationEvent; | |
| 51 | import javafx.scene.input.KeyEvent; | |
| 52 | import javafx.stage.Stage; | |
| 53 | import javafx.stage.Window; | |
| 54 | import org.greenrobot.eventbus.Subscribe; | |
| 55 | ||
| 56 | import java.io.File; | |
| 57 | import java.io.FileNotFoundException; | |
| 58 | import java.nio.file.Path; | |
| 59 | import java.util.*; | |
| 60 | import java.util.concurrent.ExecutorService; | |
| 61 | import java.util.concurrent.ScheduledExecutorService; | |
| 62 | import java.util.concurrent.ScheduledFuture; | |
| 63 | import java.util.concurrent.atomic.AtomicBoolean; | |
| 64 | import java.util.concurrent.atomic.AtomicReference; | |
| 65 | import java.util.function.Consumer; | |
| 66 | import java.util.function.Function; | |
| 67 | import java.util.stream.Collectors; | |
| 68 | ||
| 69 | import static com.keenwrite.ExportFormat.NONE; | |
| 70 | import static com.keenwrite.Launcher.terminate; | |
| 71 | import static com.keenwrite.Messages.get; | |
| 72 | import static com.keenwrite.constants.Constants.*; | |
| 73 | import static com.keenwrite.events.Bus.register; | |
| 74 | import static com.keenwrite.events.StatusEvent.clue; | |
| 75 | import static com.keenwrite.io.MediaType.*; | |
| 76 | import static com.keenwrite.io.MediaType.TypeName.TEXT; | |
| 77 | import static com.keenwrite.io.SysFile.toFile; | |
| 78 | import static com.keenwrite.preferences.AppKeys.*; | |
| 79 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 80 | import static com.keenwrite.processors.ProcessorContext.Mutator; | |
| 81 | import static com.keenwrite.processors.ProcessorContext.builder; | |
| 82 | import static com.keenwrite.processors.ProcessorFactory.createProcessors; | |
| 83 | import static java.awt.Desktop.getDesktop; | |
| 84 | import static java.util.concurrent.Executors.newFixedThreadPool; | |
| 85 | import static java.util.concurrent.Executors.newScheduledThreadPool; | |
| 86 | import static java.util.concurrent.TimeUnit.SECONDS; | |
| 87 | import static java.util.stream.Collectors.groupingBy; | |
| 88 | import static javafx.application.Platform.exit; | |
| 89 | import static javafx.application.Platform.runLater; | |
| 90 | import static javafx.scene.control.ButtonType.NO; | |
| 91 | import static javafx.scene.control.ButtonType.YES; | |
| 92 | import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS; | |
| 93 | import static javafx.scene.input.KeyCode.ENTER; | |
| 94 | import static javafx.scene.input.KeyCode.SPACE; | |
| 95 | import static javafx.scene.input.KeyCombination.ALT_DOWN; | |
| 96 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 97 | import static javafx.util.Duration.millis; | |
| 98 | import static javax.swing.SwingUtilities.invokeLater; | |
| 99 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 100 | ||
| 101 | /** | |
| 102 | * Responsible for wiring together the main application components for a | |
| 103 | * particular {@link Workspace} (project). These include the definition views, | |
| 104 | * text editors, and preview pane along with any corresponding controllers. | |
| 105 | */ | |
| 106 | public final class MainPane extends SplitPane { | |
| 107 | ||
| 108 | private static final ExecutorService sExecutor = newFixedThreadPool( 1 ); | |
| 109 | private static final Notifier sNotifier = Services.load( Notifier.class ); | |
| 110 | ||
| 111 | /** | |
| 112 | * Used when opening files to determine how each file should be binned and | |
| 113 | * therefore what tab pane to be opened within. | |
| 114 | */ | |
| 115 | private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of( | |
| 116 | TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED | |
| 117 | ); | |
| 118 | ||
| 119 | private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 ); | |
| 120 | private final AtomicReference<ScheduledFuture<?>> mSaveTask = | |
| 121 | new AtomicReference<>(); | |
| 122 | ||
| 123 | /** | |
| 124 | * Prevents re-instantiation of processing classes. | |
| 125 | */ | |
| 126 | private final Map<TextResource, Processor<String>> mProcessors = | |
| 127 | new HashMap<>(); | |
| 128 | ||
| 129 | private final Workspace mWorkspace; | |
| 130 | ||
| 131 | /** | |
| 132 | * Groups similar file type tabs together. | |
| 133 | */ | |
| 134 | private final List<TabPane> mTabPanes = new ArrayList<>(); | |
| 135 | ||
| 136 | /** | |
| 137 | * Renders the actively selected plain text editor tab. | |
| 138 | */ | |
| 139 | private final HtmlPreview mPreview; | |
| 140 | ||
| 141 | /** | |
| 142 | * Provides an interactive document outline. | |
| 143 | */ | |
| 144 | private final DocumentOutline mOutline = new DocumentOutline(); | |
| 145 | ||
| 146 | /** | |
| 147 | * Changing the active editor fires the value changed event. This allows | |
| 148 | * refreshes to happen when external definitions are modified and need to | |
| 149 | * trigger the processing chain. | |
| 150 | */ | |
| 151 | private final ObjectProperty<TextEditor> mTextEditor = | |
| 152 | new SimpleObjectProperty<>(); | |
| 153 | ||
| 154 | /** | |
| 155 | * Changing the active definition editor fires the value changed event. This | |
| 156 | * allows refreshes to happen when external definitions are modified and need | |
| 157 | * to trigger the processing chain. | |
| 158 | */ | |
| 159 | private final ObjectProperty<TextDefinition> mDefinitionEditor = | |
| 160 | new SimpleObjectProperty<>(); | |
| 161 | ||
| 162 | private final ObjectProperty<SpellChecker> mSpellChecker; | |
| 163 | ||
| 164 | private final TextEditorSpellChecker mEditorSpeller; | |
| 165 | ||
| 166 | /** | |
| 167 | * Called when the definition data is changed. | |
| 168 | */ | |
| 169 | private final EventHandler<TreeModificationEvent<Event>> mTreeHandler = | |
| 170 | _ -> { | |
| 171 | process( getTextEditor() ); | |
| 172 | save( getTextDefinition() ); | |
| 173 | }; | |
| 174 | ||
| 175 | /** | |
| 176 | * Tracks the number of detached tab panels opened into their own windows, | |
| 177 | * which allows unique identification of subordinate windows by their title. | |
| 178 | * It is doubtful more than 128 windows, much less 256, will be created. | |
| 179 | */ | |
| 180 | private byte mWindowCount; | |
| 181 | ||
| 182 | private final VariableNameInjector mVariableNameInjector; | |
| 183 | ||
| 184 | private final RBootstrapController mRBootstrapController; | |
| 185 | ||
| 186 | private final DocumentStatistics mStatistics; | |
| 187 | ||
| 188 | @SuppressWarnings( { "FieldCanBeLocal", "unused" } ) | |
| 189 | private final TypesetterInstaller mInstallWizard; | |
| 190 | ||
| 191 | /** | |
| 192 | * Adds all content panels to the main user interface. This will load the | |
| 193 | * configuration settings from the workspace to reproduce the settings from | |
| 194 | * a previous session. | |
| 195 | */ | |
| 196 | public MainPane( final Workspace workspace ) { | |
| 197 | mWorkspace = workspace; | |
| 198 | mSpellChecker = createSpellChecker(); | |
| 199 | mEditorSpeller = createTextEditorSpellChecker( mSpellChecker ); | |
| 200 | mPreview = new HtmlPreview( workspace ); | |
| 201 | mStatistics = new DocumentStatistics( workspace ); | |
| 202 | ||
| 203 | mTextEditor.addListener( ( c, o, n ) -> { | |
| 204 | if( o != null ) { | |
| 205 | removeProcessor( o ); | |
| 206 | } | |
| 207 | ||
| 208 | if( n != null ) { | |
| 209 | mPreview.setBaseUri( n.getPath() ); | |
| 210 | updateProcessors( n ); | |
| 211 | process( n ); | |
| 212 | } | |
| 213 | } ); | |
| 214 | ||
| 215 | mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) ); | |
| 216 | mDefinitionEditor.set( createDefinitionEditor( workspace ) ); | |
| 217 | mVariableNameInjector = new VariableNameInjector( workspace ); | |
| 218 | mRBootstrapController = new RBootstrapController( | |
| 219 | workspace, mDefinitionEditor.get()::getDefinitions | |
| 220 | ); | |
| 221 | ||
| 222 | // If the user modifies the definitions, re-process the variables. | |
| 223 | mDefinitionEditor.addListener( ( c, o, n ) -> { | |
| 224 | final var textEditor = getTextEditor(); | |
| 225 | ||
| 226 | if( textEditor.isMediaType( TEXT_R_MARKDOWN ) ) { | |
| 227 | mRBootstrapController.update(); | |
| 228 | } | |
| 229 | ||
| 230 | process( textEditor ); | |
| 231 | } ); | |
| 232 | ||
| 233 | open( collect( getRecentFiles() ) ); | |
| 234 | viewPreview(); | |
| 235 | setDividerPositions( calculateDividerPositions() ); | |
| 236 | ||
| 237 | // Once the main scene's window regains focus, update the active definition | |
| 238 | // editor to the currently selected tab. | |
| 239 | runLater( () -> getWindow().setOnCloseRequest( event -> { | |
| 240 | // Order matters: Open file names must be persisted before closing all. | |
| 241 | mWorkspace.save(); | |
| 242 | ||
| 243 | if( closeAll() ) { | |
| 244 | exit(); | |
| 245 | terminate( 0 ); | |
| 246 | } | |
| 247 | ||
| 248 | event.consume(); | |
| 249 | } ) ); | |
| 250 | ||
| 251 | register( this ); | |
| 252 | initAutosave( workspace ); | |
| 253 | ||
| 254 | restoreSession(); | |
| 255 | runLater( this::restoreFocus ); | |
| 256 | ||
| 257 | mInstallWizard = new TypesetterInstaller( workspace ); | |
| 258 | } | |
| 259 | ||
| 260 | /** | |
| 261 | * Called when spellchecking can be run. This will reload the dictionary | |
| 262 | * into memory once, and then re-use it for all the existing text editors. | |
| 263 | * | |
| 264 | * @param event The event to process, having a populated word-frequency map. | |
| 265 | */ | |
| 266 | @Subscribe | |
| 267 | public void handle( final LexiconLoadedEvent event ) { | |
| 268 | final var lexicon = event.getLexicon(); | |
| 269 | ||
| 270 | try { | |
| 271 | final var checker = SymSpellSpeller.forLexicon( lexicon ); | |
| 272 | mSpellChecker.set( checker ); | |
| 273 | } catch( final Exception ex ) { | |
| 274 | clue( ex ); | |
| 275 | } | |
| 276 | } | |
| 277 | ||
| 278 | @Subscribe | |
| 279 | public void handle( final TextEditorFocusEvent event ) { | |
| 280 | mTextEditor.set( event.get() ); | |
| 281 | } | |
| 282 | ||
| 283 | @Subscribe | |
| 284 | public void handle( final TextDefinitionFocusEvent event ) { | |
| 285 | mDefinitionEditor.set( event.get() ); | |
| 286 | } | |
| 287 | ||
| 288 | /** | |
| 289 | * Typically called when a file name is clicked in the preview panel. | |
| 290 | * | |
| 291 | * @param event The event to process, must contain a valid file reference. | |
| 292 | */ | |
| 293 | @Subscribe | |
| 294 | public void handle( final FileOpenEvent event ) { | |
| 295 | final File eventFile; | |
| 296 | final var eventUri = event.getUri(); | |
| 297 | ||
| 298 | if( eventUri.isAbsolute() ) { | |
| 299 | eventFile = new File( eventUri.getPath() ); | |
| 300 | } | |
| 301 | else { | |
| 302 | final var activeFile = getTextEditor().getFile(); | |
| 303 | final var parent = activeFile.getParentFile(); | |
| 304 | ||
| 305 | if( parent == null ) { | |
| 306 | clue( new FileNotFoundException( eventUri.getPath() ) ); | |
| 307 | return; | |
| 308 | } | |
| 309 | else { | |
| 310 | final var parentPath = parent.getAbsolutePath(); | |
| 311 | eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) ); | |
| 312 | } | |
| 313 | } | |
| 314 | ||
| 315 | final var mediaType = MediaTypeExtension.fromFile( eventFile ); | |
| 316 | ||
| 317 | runLater( () -> { | |
| 318 | // Open text files locally. | |
| 319 | if( mediaType.isType( TEXT ) ) { | |
| 320 | open( eventFile ); | |
| 321 | } | |
| 322 | else { | |
| 323 | try { | |
| 324 | // Delegate opening all other file types to the operating system. | |
| 325 | getDesktop().open( eventFile ); | |
| 326 | } catch( final Exception ex ) { | |
| 327 | clue( ex ); | |
| 328 | } | |
| 329 | } | |
| 330 | } ); | |
| 331 | } | |
| 332 | ||
| 333 | @Subscribe | |
| 334 | public void handle( final CaretNavigationEvent event ) { | |
| 335 | runLater( () -> { | |
| 336 | final var textArea = getTextEditor(); | |
| 337 | textArea.moveTo( event.getOffset() ); | |
| 338 | textArea.requestFocus(); | |
| 339 | } ); | |
| 340 | } | |
| 341 | ||
| 342 | @Subscribe | |
| 343 | public void handle( final InsertDefinitionEvent<String> event ) { | |
| 344 | final var leaf = event.getLeaf(); | |
| 345 | final var editor = mTextEditor.get(); | |
| 346 | ||
| 347 | mVariableNameInjector.insert( editor, leaf ); | |
| 348 | } | |
| 349 | ||
| 350 | private void initAutosave( final Workspace workspace ) { | |
| 351 | final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE ); | |
| 352 | ||
| 353 | rate.addListener( | |
| 354 | ( c, o, n ) -> { | |
| 355 | final var taskRef = mSaveTask.get(); | |
| 356 | ||
| 357 | // Prevent multiple auto-saves from running. | |
| 358 | if( taskRef != null ) { | |
| 359 | taskRef.cancel( false ); | |
| 360 | } | |
| 361 | ||
| 362 | initAutosave( rate ); | |
| 363 | } | |
| 364 | ); | |
| 365 | ||
| 366 | // Start the save listener (avoids duplicating some code). | |
| 367 | initAutosave( rate ); | |
| 368 | } | |
| 369 | ||
| 370 | private void initAutosave( final IntegerProperty rate ) { | |
| 371 | mSaveTask.set( | |
| 372 | mSaver.scheduleAtFixedRate( | |
| 373 | () -> { | |
| 374 | if( getTextEditor().isModified() ) { | |
| 375 | // Ensure the modified indicator is cleared by running on EDT. | |
| 376 | runLater( this::save ); | |
| 377 | } | |
| 378 | }, 0, rate.intValue(), SECONDS | |
| 379 | ) | |
| 380 | ); | |
| 381 | } | |
| 382 | ||
| 383 | /** | |
| 384 | * TODO: Load divider positions from exported settings, see | |
| 385 | * {@link #collect(SetProperty)} comment. | |
| 386 | */ | |
| 387 | private double[] calculateDividerPositions() { | |
| 388 | final var ratio = 100f / getItems().size() / 100; | |
| 389 | final var positions = getDividerPositions(); | |
| 390 | ||
| 391 | for( int i = 0; i < positions.length; i++ ) { | |
| 392 | positions[ i ] = ratio * i; | |
| 393 | } | |
| 394 | ||
| 395 | return positions; | |
| 396 | } | |
| 397 | ||
| 398 | /** | |
| 399 | * Opens all the files into the application, provided the paths are unique. | |
| 400 | * This may only be called for any type of files that a user can edit | |
| 401 | * (i.e., update and persist), such as definitions and text files. | |
| 402 | * | |
| 403 | * @param files The list of files to open. | |
| 404 | */ | |
| 405 | public void open( final List<File> files ) { | |
| 406 | files.forEach( this::open ); | |
| 407 | } | |
| 408 | ||
| 409 | /** | |
| 410 | * This opens the given file. Since the preview pane is not a file that | |
| 411 | * can be opened, it is safe to add a listener to the detachable pane. | |
| 412 | * This will exit early if the given file is not a regular file (i.e., a | |
| 413 | * directory). | |
| 414 | * | |
| 415 | * @param inputFile The file to open. | |
| 416 | */ | |
| 417 | private void open( final File inputFile ) { | |
| 418 | // Prevent opening directories (a non-existent "untitled.md" is fine). | |
| 419 | if( !inputFile.isFile() && inputFile.exists() ) { | |
| 420 | return; | |
| 421 | } | |
| 422 | ||
| 423 | final var mediaType = fromFilename( inputFile ); | |
| 424 | ||
| 425 | // Only allow opening text files. | |
| 426 | if( !mediaType.isType( TEXT ) ) { | |
| 427 | return; | |
| 428 | } | |
| 429 | ||
| 430 | final var tab = createTab( inputFile ); | |
| 431 | final var node = tab.getContent(); | |
| 432 | final var tabPane = obtainTabPane( mediaType ); | |
| 433 | ||
| 434 | tab.setTooltip( createTooltip( inputFile ) ); | |
| 435 | tabPane.setFocusTraversable( false ); | |
| 436 | tabPane.setTabClosingPolicy( ALL_TABS ); | |
| 437 | tabPane.getTabs().add( tab ); | |
| 438 | ||
| 439 | // Attach the tab scene factory for new tab panes. | |
| 440 | if( !getItems().contains( tabPane ) ) { | |
| 441 | addTabPane( | |
| 442 | node instanceof TextDefinition ? 0 : getItems().size(), tabPane | |
| 443 | ); | |
| 444 | } | |
| 445 | ||
| 446 | if( inputFile.isFile() ) { | |
| 447 | getRecentFiles().add( inputFile.getAbsolutePath() ); | |
| 448 | } | |
| 449 | } | |
| 450 | ||
| 451 | /** | |
| 452 | * Gives focus to the most recently edited document and attempts to move | |
| 453 | * the caret to the most recently known offset into said document. | |
| 454 | */ | |
| 455 | private void restoreSession() { | |
| 456 | final var workspace = getWorkspace(); | |
| 457 | final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT ); | |
| 458 | final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET ); | |
| 459 | ||
| 460 | for( final var pane : mTabPanes ) { | |
| 461 | for( final var tab : pane.getTabs() ) { | |
| 462 | final var tooltip = tab.getTooltip(); | |
| 463 | ||
| 464 | if( tooltip != null ) { | |
| 465 | final var tabName = tooltip.getText(); | |
| 466 | final var fileName = file.get().toString(); | |
| 467 | ||
| 468 | if( tabName.equalsIgnoreCase( fileName ) ) { | |
| 469 | final var node = tab.getContent(); | |
| 470 | ||
| 471 | pane.getSelectionModel().select( tab ); | |
| 472 | node.requestFocus(); | |
| 473 | ||
| 474 | if( node instanceof TextEditor editor ) { | |
| 475 | runLater( () -> editor.moveTo( offset.getValue() ) ); | |
| 476 | } | |
| 477 | ||
| 478 | break; | |
| 479 | } | |
| 480 | } | |
| 481 | } | |
| 482 | } | |
| 483 | } | |
| 484 | ||
| 485 | /** | |
| 486 | * Sets the focus to the middle pane, which contains the text editor tabs. | |
| 487 | */ | |
| 488 | private void restoreFocus() { | |
| 489 | // Work around a bug where focusing directly on the middle pane results | |
| 490 | // in the R engine not loading variables properly. | |
| 491 | mTabPanes.get( 0 ).requestFocus(); | |
| 492 | ||
| 493 | // This is the only line that should be required. | |
| 494 | mTabPanes.get( 1 ).requestFocus(); | |
| 495 | } | |
| 496 | ||
| 497 | /** | |
| 498 | * Opens a new text editor document using a document file name that doesn't | |
| 499 | * clash with an existing document. | |
| 500 | */ | |
| 501 | public void newTextEditor() { | |
| 502 | final String key = "file.default.document."; | |
| 503 | final String prefix = Constants.get( STR."\{key}prefix" ); | |
| 504 | final String suffix = Constants.get( STR."\{key}suffix" ); | |
| 505 | ||
| 506 | File file = new File( STR."\{prefix}.\{suffix}" ); | |
| 507 | int i = 0; | |
| 508 | ||
| 509 | while( file.exists() && i++ < 100 ) { | |
| 510 | file = new File( STR."\{prefix}-\{i}.\{suffix}" ); | |
| 511 | } | |
| 512 | ||
| 513 | open( file ); | |
| 501 | 514 | } |
| 502 | 515 |
| 259 | 259 | } |
| 260 | 260 | |
| 261 | static String get( final String key ) { | |
| 261 | public static String get( final String key ) { | |
| 262 | 262 | return sSettings.getSetting( key, "" ); |
| 263 | 263 | } |
| ... | ||
| 270 | 270 | */ |
| 271 | 271 | private static File getFile( final String suffix ) { |
| 272 | return new File( get( "file.default." + suffix ) ); | |
| 272 | return new File( get( STR."file.default.\{suffix}" ) ); | |
| 273 | 273 | } |
| 274 | 274 | |
| 67 | 67 | */ |
| 68 | 68 | public String getProblem() { |
| 69 | // 256 is arbitrary; stack traces shouldn't be much larger. | |
| 70 | final var sb = new StringBuilder( 256 ); | |
| 69 | // Arbitrary limit. | |
| 70 | final var sb = new StringBuilder( 1024 ); | |
| 71 | 71 | final var trace = mProblem; |
| 72 | 72 | |
| 73 | 73 | if( trace != null ) { |
| 74 | 74 | stream( trace.getStackTrace() ) |
| 75 | .takeWhile( StatusEvent::filter ) | |
| 76 | .limit( 15 ) | |
| 75 | .limit( 150 ) | |
| 77 | 76 | .toList() |
| 78 | 77 | .forEach( e -> sb.append( e.toString() ).append( NEWLINE ) ); |
| ... | ||
| 91 | 90 | message.isBlank() ? "" : " ", |
| 92 | 91 | mProblem == null ? "" : toEnglish( mProblem ) ); |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * Returns {@code true} to allow the {@link StackTraceElement} to pass | |
| 97 | * through the filter. | |
| 98 | * | |
| 99 | * @param e The element to check against the filter. | |
| 100 | */ | |
| 101 | private static boolean filter( final StackTraceElement e ) { | |
| 102 | final var clazz = e.getClassName(); | |
| 103 | return !(clazz.contains( "org.renjin." ) || | |
| 104 | clazz.contains( "sun." ) || | |
| 105 | clazz.contains( "flexmark." ) || | |
| 106 | clazz.contains( "java." ) | |
| 107 | ); | |
| 108 | 92 | } |
| 109 | 93 | |
| 48 | 48 | */ |
| 49 | 49 | public Builder setId( final String id ) { |
| 50 | final var prefix = ACTION_PREFIX + id + "."; | |
| 51 | final var text = prefix + "text"; | |
| 52 | final var icon = prefix + "icon"; | |
| 53 | final var accelerator = prefix + "accelerator"; | |
| 50 | final var prefix = STR."\{ACTION_PREFIX}\{id}."; | |
| 51 | final var text = STR."\{prefix}text"; | |
| 52 | final var icon = STR."\{prefix}icon"; | |
| 53 | final var accelerator = STR."\{prefix}accelerator"; | |
| 54 | 54 | final var builder = setText( text ).setIcon( icon ); |
| 55 | 55 | |
| ... | ||
| 172 | 172 | |
| 173 | 173 | if( mAccelerator != null ) { |
| 174 | tooltip += " (" + mAccelerator.getDisplayText() + ')'; | |
| 174 | tooltip += STR." (\{mAccelerator.getDisplayText()}\{')'}"; | |
| 175 | 175 | } |
| 176 | 176 | |
| 59 | 59 | return createMenu( |
| 60 | 60 | get( "Main.menu.file" ), |
| 61 | addAction( "file.new", e -> actions.file_new() ), | |
| 62 | addAction( "file.open", e -> actions.file_open() ), | |
| 61 | addAction( "file.new", _ -> actions.file_new() ), | |
| 62 | addAction( "file.open", _ -> actions.file_open() ), | |
| 63 | 63 | SEPARATOR, |
| 64 | addAction( "file.close", e -> actions.file_close() ), | |
| 65 | addAction( "file.close_all", e -> actions.file_close_all() ), | |
| 64 | addAction( "file.close", _ -> actions.file_close() ), | |
| 65 | addAction( "file.close_all", _ -> actions.file_close_all() ), | |
| 66 | 66 | SEPARATOR, |
| 67 | addAction( "file.save", e -> actions.file_save() ), | |
| 68 | addAction( "file.save_as", e -> actions.file_save_as() ), | |
| 69 | addAction( "file.save_all", e -> actions.file_save_all() ), | |
| 67 | addAction( "file.save", _ -> actions.file_save() ), | |
| 68 | addAction( "file.save_as", _ -> actions.file_save_as() ), | |
| 69 | addAction( "file.save_all", _ -> actions.file_save_all() ), | |
| 70 | 70 | SEPARATOR, |
| 71 | addAction( "file.export", e -> { } ) | |
| 71 | addAction( "file.export", _ -> { } ) | |
| 72 | 72 | .addSubActions( |
| 73 | addAction( "file.export.pdf", e -> actions.file_export_pdf() ), | |
| 74 | addAction( "file.export.pdf.dir", e -> actions.file_export_pdf_dir() ), | |
| 75 | addAction( "file.export.pdf.repeat", e -> actions.file_export_repeat() ), | |
| 76 | addAction( "file.export.html.dir", e -> actions.file_export_html_dir() ), | |
| 77 | addAction( "file.export.html_svg", e -> actions.file_export_html_svg() ), | |
| 78 | addAction( "file.export.html_tex", e -> actions.file_export_html_tex() ), | |
| 79 | addAction( "file.export.xhtml_tex", e -> actions.file_export_xhtml_tex() ) | |
| 73 | addAction( "file.export.pdf", _ -> actions.file_export_pdf() ), | |
| 74 | addAction( "file.export.pdf.dir", _ -> actions.file_export_pdf_dir() ), | |
| 75 | addAction( "file.export.pdf.repeat", _ -> actions.file_export_repeat() ), | |
| 76 | addAction( "file.export.html.dir", _ -> actions.file_export_html_dir() ), | |
| 77 | addAction( "file.export.html_svg", _ -> actions.file_export_html_svg() ), | |
| 78 | addAction( "file.export.html_tex", _ -> actions.file_export_html_tex() ), | |
| 79 | addAction( "file.export.xhtml_tex", _ -> actions.file_export_xhtml_tex() ) | |
| 80 | 80 | ), |
| 81 | 81 | SEPARATOR, |
| 82 | addAction( "file.exit", e -> actions.file_exit() ) | |
| 82 | addAction( "file.exit", _ -> actions.file_exit() ) | |
| 83 | 83 | ); |
| 84 | 84 | // @formatter:on |
| ... | ||
| 91 | 91 | get( "Main.menu.edit" ), |
| 92 | 92 | SEPARATOR, |
| 93 | addAction( "edit.undo", e -> actions.edit_undo() ), | |
| 94 | addAction( "edit.redo", e -> actions.edit_redo() ), | |
| 93 | addAction( "edit.undo", _ -> actions.edit_undo() ), | |
| 94 | addAction( "edit.redo", _ -> actions.edit_redo() ), | |
| 95 | 95 | SEPARATOR, |
| 96 | addAction( "edit.cut", e -> actions.edit_cut() ), | |
| 97 | addAction( "edit.copy", e -> actions.edit_copy() ), | |
| 98 | addAction( "edit.paste", e -> actions.edit_paste() ), | |
| 99 | addAction( "edit.select_all", e -> actions.edit_select_all() ), | |
| 96 | addAction( "edit.cut", _ -> actions.edit_cut() ), | |
| 97 | addAction( "edit.copy", _ -> actions.edit_copy() ), | |
| 98 | addAction( "edit.paste", _ -> actions.edit_paste() ), | |
| 99 | addAction( "edit.select_all", _ -> actions.edit_select_all() ), | |
| 100 | 100 | SEPARATOR, |
| 101 | addAction( "edit.find", e -> actions.edit_find() ), | |
| 102 | addAction( "edit.find_next", e -> actions.edit_find_next() ), | |
| 103 | addAction( "edit.find_prev", e -> actions.edit_find_prev() ), | |
| 101 | addAction( "edit.find", _ -> actions.edit_find() ), | |
| 102 | addAction( "edit.find_next", _ -> actions.edit_find_next() ), | |
| 103 | addAction( "edit.find_prev", _ -> actions.edit_find_prev() ), | |
| 104 | 104 | SEPARATOR, |
| 105 | addAction( "edit.preferences", e -> actions.edit_preferences() ) | |
| 105 | addAction( "edit.preferences", _ -> actions.edit_preferences() ) | |
| 106 | 106 | ); |
| 107 | 107 | } |
| 108 | 108 | |
| 109 | 109 | @NotNull |
| 110 | 110 | private static Menu createMenuFormat( final GuiCommands actions ) { |
| 111 | 111 | return createMenu( |
| 112 | 112 | get( "Main.menu.format" ), |
| 113 | addAction( "format.bold", e -> actions.format_bold() ), | |
| 114 | addAction( "format.italic", e -> actions.format_italic() ), | |
| 115 | addAction( "format.monospace", e -> actions.format_monospace() ), | |
| 116 | addAction( "format.superscript", e -> actions.format_superscript() ), | |
| 117 | addAction( "format.subscript", e -> actions.format_subscript() ), | |
| 118 | addAction( "format.strikethrough", e -> actions.format_strikethrough() ) | |
| 113 | addAction( "format.bold", _ -> actions.format_bold() ), | |
| 114 | addAction( "format.italic", _ -> actions.format_italic() ), | |
| 115 | addAction( "format.monospace", _ -> actions.format_monospace() ), | |
| 116 | addAction( "format.superscript", _ -> actions.format_superscript() ), | |
| 117 | addAction( "format.subscript", _ -> actions.format_subscript() ), | |
| 118 | addAction( "format.strikethrough", _ -> actions.format_strikethrough() ) | |
| 119 | 119 | ); |
| 120 | 120 | } |
| ... | ||
| 127 | 127 | return createMenu( |
| 128 | 128 | get( "Main.menu.insert" ), |
| 129 | addAction( "insert.blockquote", e -> actions.insert_blockquote() ), | |
| 130 | addAction( "insert.code", e -> actions.insert_code() ), | |
| 131 | addAction( "insert.fenced_code_block", e -> actions.insert_fenced_code_block() ), | |
| 129 | addAction( "insert.blockquote", _ -> actions.insert_blockquote() ), | |
| 130 | addAction( "insert.code", _ -> actions.insert_code() ), | |
| 131 | addAction( "insert.fenced_code_block", _ -> actions.insert_fenced_code_block() ), | |
| 132 | 132 | SEPARATOR, |
| 133 | addAction( "insert.link", e -> actions.insert_link() ), | |
| 134 | addAction( "insert.image", e -> actions.insert_image() ), | |
| 133 | addAction( "insert.link", _ -> actions.insert_link() ), | |
| 134 | addAction( "insert.image", _ -> actions.insert_image() ), | |
| 135 | 135 | SEPARATOR, |
| 136 | addAction( "insert.heading_1", e -> actions.insert_heading_1() ), | |
| 137 | addAction( "insert.heading_2", e -> actions.insert_heading_2() ), | |
| 138 | addAction( "insert.heading_3", e -> actions.insert_heading_3() ), | |
| 136 | addAction( "insert.heading_1", _ -> actions.insert_heading_1() ), | |
| 137 | addAction( "insert.heading_2", _ -> actions.insert_heading_2() ), | |
| 138 | addAction( "insert.heading_3", _ -> actions.insert_heading_3() ), | |
| 139 | 139 | SEPARATOR, |
| 140 | addAction( "insert.unordered_list", e -> actions.insert_unordered_list() ), | |
| 141 | addAction( "insert.ordered_list", e -> actions.insert_ordered_list() ), | |
| 142 | addAction( "insert.horizontal_rule", e -> actions.insert_horizontal_rule() ) | |
| 140 | addAction( "insert.unordered_list", _ -> actions.insert_unordered_list() ), | |
| 141 | addAction( "insert.ordered_list", _ -> actions.insert_ordered_list() ), | |
| 142 | addAction( "insert.horizontal_rule", _ -> actions.insert_horizontal_rule() ) | |
| 143 | 143 | ); |
| 144 | 144 | // @formatter:on |
| 145 | 145 | } |
| 146 | 146 | |
| 147 | 147 | @NotNull |
| 148 | 148 | private static Menu createMenuVariable( |
| 149 | 149 | final GuiCommands actions, final SeparatorAction SEPARATOR ) { |
| 150 | 150 | return createMenu( |
| 151 | 151 | get( "Main.menu.definition" ), |
| 152 | addAction( "definition.insert", e -> actions.definition_autoinsert() ), | |
| 152 | addAction( "definition.insert", _ -> actions.definition_autoinsert() ), | |
| 153 | 153 | SEPARATOR, |
| 154 | addAction( "definition.create", e -> actions.definition_create() ), | |
| 155 | addAction( "definition.rename", e -> actions.definition_rename() ), | |
| 156 | addAction( "definition.delete", e -> actions.definition_delete() ) | |
| 154 | addAction( "definition.create", _ -> actions.definition_create() ), | |
| 155 | addAction( "definition.rename", _ -> actions.definition_rename() ), | |
| 156 | addAction( "definition.delete", _ -> actions.definition_delete() ) | |
| 157 | 157 | ); |
| 158 | 158 | } |
| 159 | 159 | |
| 160 | 160 | @NotNull |
| 161 | 161 | private static Menu createMenuView( |
| 162 | 162 | final GuiCommands actions, final SeparatorAction SEPARATOR ) { |
| 163 | 163 | return createMenu( |
| 164 | 164 | get( "Main.menu.view" ), |
| 165 | addAction( "view.refresh", e -> actions.view_refresh() ), | |
| 165 | addAction( "view.refresh", _ -> actions.view_refresh() ), | |
| 166 | 166 | SEPARATOR, |
| 167 | addAction( "view.preview", e -> actions.view_preview() ), | |
| 168 | addAction( "view.outline", e -> actions.view_outline() ), | |
| 169 | addAction( "view.statistics", e -> actions.view_statistics() ), | |
| 170 | addAction( "view.files", e -> actions.view_files() ), | |
| 167 | addAction( "view.preview", _ -> actions.view_preview() ), | |
| 168 | addAction( "view.outline", _ -> actions.view_outline() ), | |
| 169 | addAction( "view.statistics", _ -> actions.view_statistics() ), | |
| 170 | addAction( "view.files", _ -> actions.view_files() ), | |
| 171 | 171 | SEPARATOR, |
| 172 | addAction( "view.menubar", e -> actions.view_menubar() ), | |
| 173 | addAction( "view.toolbar", e -> actions.view_toolbar() ), | |
| 174 | addAction( "view.statusbar", e -> actions.view_statusbar() ), | |
| 172 | addAction( "view.menubar", _ -> actions.view_menubar() ), | |
| 173 | addAction( "view.toolbar", _ -> actions.view_toolbar() ), | |
| 174 | addAction( "view.statusbar", _ -> actions.view_statusbar() ), | |
| 175 | 175 | SEPARATOR, |
| 176 | addAction( "view.log", e -> actions.view_log() ) | |
| 176 | addAction( "view.log", _ -> actions.view_log() ) | |
| 177 | 177 | ); |
| 178 | 178 | } |
| 179 | 179 | |
| 180 | 180 | @NotNull |
| 181 | 181 | private static Menu createMenuHelp( final GuiCommands actions ) { |
| 182 | 182 | return createMenu( |
| 183 | 183 | get( "Main.menu.help" ), |
| 184 | addAction( "help.about", e -> actions.help_about() ) | |
| 184 | addAction( "help.about", _ -> actions.help_about() ) | |
| 185 | 185 | ); |
| 186 | 186 | } |
| 12 | 12 | |
| 13 | 13 | workspace.document.meta=Document Metadata |
| 14 | workspace.document.meta.desc=Keys must be alphabetic, values may use variables (e.g., '{{'book.title'}}'). | |
| 15 | workspace.document.meta.title=Pairs | |
| 16 | ||
| 17 | workspace.editor=Editor | |
| 18 | workspace.editor.autosave=Autosave | |
| 19 | workspace.editor.autosave.desc=Amount of time to wait between saves, in seconds (0 means disabled). | |
| 20 | workspace.editor.autosave.title=Timeout | |
| 21 | ||
| 22 | workspace.typeset=Typesetting | |
| 23 | workspace.typeset.context=ConTeXt | |
| 24 | workspace.typeset.context.themes.path=Paths | |
| 25 | workspace.typeset.context.themes.path.desc=Directory containing theme subdirectories. | |
| 26 | workspace.typeset.context.themes.path.title=Themes | |
| 27 | workspace.typeset.context.clean=Clean | |
| 28 | workspace.typeset.context.clean.desc=Delete ancillary files after an unsuccessful export. | |
| 29 | workspace.typeset.context.clean.title=Purge | |
| 30 | workspace.typeset.context.fonts=Fonts | |
| 31 | workspace.typeset.context.fonts.dir=Directory | |
| 32 | workspace.typeset.context.fonts.dir.desc=Directory containing additional font files (OTF and TTF). | |
| 33 | workspace.typeset.context.fonts.dir.title=Path | |
| 34 | workspace.typeset.typography=Typography | |
| 35 | workspace.typeset.typography.quotes=Quotation Marks | |
| 36 | workspace.typeset.typography.quotes.desc=Export straight quotes and apostrophes as curled equivalents. | |
| 37 | workspace.typeset.typography.quotes.title=Curl | |
| 38 | ||
| 39 | workspace.r=R | |
| 40 | workspace.r.script=Startup Script | |
| 41 | workspace.r.script.desc=Script runs prior to executing R statements within the document. | |
| 42 | workspace.r.dir=Working Directory | |
| 43 | workspace.r.dir.desc=Value assigned to v$application$r$working$directory and usable in the startup script. | |
| 44 | workspace.r.dir.title=Directory | |
| 45 | workspace.r.delimiter.began=Delimiter Prefix | |
| 46 | workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted variables. | |
| 47 | workspace.r.delimiter.began.title=Opening | |
| 48 | workspace.r.delimiter.ended=Delimiter Suffix | |
| 49 | workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted variables. | |
| 50 | workspace.r.delimiter.ended.title=Closing | |
| 51 | ||
| 52 | workspace.images=Images | |
| 53 | workspace.images.dir=Absolute Directory | |
| 54 | workspace.images.dir.desc=Path to search for local file system images. | |
| 55 | workspace.images.dir.title=Directory | |
| 56 | workspace.images.cache.desc=Path to store remotely retrieved images. | |
| 57 | workspace.images.cache.title=Directory | |
| 58 | workspace.images.order=Extensions | |
| 59 | workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces. | |
| 60 | workspace.images.order.title=Extensions | |
| 61 | workspace.images.resize=Resize | |
| 62 | workspace.images.resize.desc=Scale images to fit the preview panel when resizing, automatically. | |
| 63 | workspace.images.resize.title=Resize | |
| 64 | workspace.images.server=Diagram Server | |
| 65 | workspace.images.server.desc=Server used to generate diagrams (e.g., kroki.io). | |
| 66 | workspace.images.server.title=Name | |
| 67 | ||
| 68 | workspace.definition=Variable | |
| 69 | workspace.definition.path=File name | |
| 70 | workspace.definition.path.desc=Absolute path to interpolated string variables. | |
| 71 | workspace.definition.path.title=Path | |
| 72 | workspace.definition.delimiter.began=Delimiter Prefix | |
| 73 | workspace.definition.delimiter.began.desc=Indicates when a variable name is starting. | |
| 74 | workspace.definition.delimiter.began.title=Opening | |
| 75 | workspace.definition.delimiter.ended=Delimiter Suffix | |
| 76 | workspace.definition.delimiter.ended.desc=Indicates when a variable name is ending. | |
| 77 | workspace.definition.delimiter.ended.title=Closing | |
| 78 | ||
| 79 | workspace.ui.skin=Skins | |
| 80 | workspace.ui.skin.selection=Bundled | |
| 81 | workspace.ui.skin.selection.desc=Pre-packaged application style (default: Modena Light). | |
| 82 | workspace.ui.skin.selection.title=Name | |
| 83 | workspace.ui.skin.custom=Custom | |
| 84 | workspace.ui.skin.custom.desc=User-defined JavaFX cascading stylesheet file. | |
| 85 | workspace.ui.skin.custom.title=Path | |
| 86 | ||
| 87 | workspace.ui.preview=Preview | |
| 88 | workspace.ui.preview.stylesheet=Stylesheet | |
| 89 | workspace.ui.preview.stylesheet.desc=User-defined HTML cascading stylesheet file. | |
| 90 | workspace.ui.preview.stylesheet.title=Path | |
| 91 | ||
| 92 | workspace.ui.font=Fonts | |
| 93 | workspace.ui.font.editor=Editor Font | |
| 94 | workspace.ui.font.editor.name=Name | |
| 95 | workspace.ui.font.editor.name.desc=Text editor font name (sans-serif font recommended). | |
| 96 | workspace.ui.font.editor.name.title=Family | |
| 97 | workspace.ui.font.editor.size=Size | |
| 98 | workspace.ui.font.editor.size.desc=Font size. | |
| 99 | workspace.ui.font.editor.size.title=Points | |
| 100 | workspace.ui.font.preview=Preview Font | |
| 101 | workspace.ui.font.preview.name=Name | |
| 102 | workspace.ui.font.preview.name.desc=Preview pane font name (must support ligatures, serif font recommended). | |
| 103 | workspace.ui.font.preview.name.title=Family | |
| 104 | workspace.ui.font.preview.size=Size | |
| 105 | workspace.ui.font.preview.size.desc=Font size. | |
| 106 | workspace.ui.font.preview.size.title=Points | |
| 107 | workspace.ui.font.preview.mono.name=Name | |
| 108 | workspace.ui.font.preview.mono.name.desc=Monospace font name. | |
| 109 | workspace.ui.font.preview.mono.name.title=Family | |
| 110 | workspace.ui.font.preview.mono.size=Size | |
| 111 | workspace.ui.font.preview.mono.size.desc=Monospace font size. | |
| 112 | workspace.ui.font.preview.mono.size.title=Points | |
| 113 | workspace.ui.font.math=Math Font | |
| 114 | workspace.ui.font.math.size.title=Scale | |
| 115 | ||
| 116 | workspace.language=Language | |
| 117 | workspace.language.locale=Internationalization | |
| 118 | workspace.language.locale.desc=Language for application and HTML export. | |
| 119 | workspace.language.locale.title=Locale | |
| 120 | ||
| 121 | # ######################################################################## | |
| 122 | # Editor actions | |
| 123 | # ######################################################################## | |
| 124 | ||
| 125 | Editor.spelling.check.matches.none=No suggestions for ''{0}'' found. | |
| 126 | Editor.spelling.check.matches.okay=The spelling for ''{0}'' appears to be correct. | |
| 127 | ||
| 128 | # ######################################################################## | |
| 129 | # Menu Bar | |
| 130 | # ######################################################################## | |
| 131 | ||
| 132 | Main.menu.file=_File | |
| 133 | Main.menu.edit=_Edit | |
| 134 | Main.menu.insert=_Insert | |
| 135 | Main.menu.format=Forma_t | |
| 136 | Main.menu.definition=_Variable | |
| 137 | Main.menu.view=Vie_w | |
| 138 | Main.menu.help=_Help | |
| 139 | ||
| 140 | # ######################################################################## | |
| 141 | # Detachable Tabs | |
| 142 | # ######################################################################## | |
| 143 | ||
| 144 | # {0} is the application title; {1} is a unique window ID. | |
| 145 | Detach.tab.title={0} - {1} | |
| 146 | ||
| 147 | # ######################################################################## | |
| 148 | # Status Bar | |
| 149 | # ######################################################################## | |
| 150 | ||
| 151 | Main.status.text.offset=offset | |
| 152 | Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2} | |
| 153 | Main.status.state.default=OK | |
| 154 | Main.status.export.success=Saved as ''{0}'' | |
| 155 | ||
| 156 | Main.status.error.bootstrap.eval=Note: Bootstrap variable of ''{0}'' not found | |
| 157 | Main.status.error.bootstrap.cache=Could not create cache directory ''{0}'' | |
| 158 | ||
| 159 | Main.status.error.parse=Evaluation error: {0} | |
| 160 | Main.status.error.def.blank=Move the caret to a word before inserting a variable | |
| 161 | Main.status.error.def.empty=Create a variable before inserting one | |
| 162 | Main.status.error.def.missing=No variable value found for ''{0}'' | |
| 163 | Main.status.error.r=Error with [{0}...]: {1} | |
| 164 | ||
| 165 | Main.status.error.file.missing=Not found: ''{0}'' | |
| 166 | Main.status.error.file.missing.near=Not found: ''{0}'' near line {1} | |
| 167 | Main.status.error.file.delete=Failed to delete ''{0}'' | |
| 168 | ||
| 169 | Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}'' | |
| 170 | Main.status.error.messages.syntax=Missing ''}'' in ''{0}'' | |
| 171 | ||
| 172 | Main.status.error.undo=Cannot undo; beginning of undo history reached | |
| 173 | Main.status.error.redo=Cannot redo; end of redo history reached | |
| 174 | ||
| 175 | Main.status.error.theme.missing=Install themes before exporting (no themes found at ''{0}'') | |
| 176 | Main.status.error.theme.name=Cannot find theme name for ''{0}'' | |
| 177 | ||
| 178 | Main.status.image.request.init=Initializing HTTP request | |
| 179 | Main.status.image.request.fetch=Downloaded image ''{0}'' | |
| 180 | Main.status.image.request.success=Determined content type ''{0}'' | |
| 181 | Main.status.image.request.error.media=No media type for ''{0}'' | |
| 182 | Main.status.image.request.error.cert=Could not accept certificate for ''{0}'' | |
| 183 | Main.status.image.request.error.rasterize=Rasterizer could not parse SVG image | |
| 184 | ||
| 185 | Main.status.image.xhtml.image.download=Downloading ''{0}'' | |
| 186 | Main.status.image.xhtml.image.resolve=Qualify path for ''{0}'' | |
| 187 | Main.status.image.xhtml.image.found=Found image ''{0}'' | |
| 188 | Main.status.image.xhtml.image.missing=Missing image ''{0}'' | |
| 189 | Main.status.image.xhtml.image.saved=Saved image ''{0}'' | |
| 190 | Main.status.image.xhtml.image.failed=Cannot save image ''{0}'' | |
| 191 | ||
| 192 | Main.status.font.search.missing=No font name starting with ''{0}'' was found | |
| 193 | ||
| 194 | Main.status.export.concat=Concatenating ''{0}'' | |
| 195 | Main.status.export.concat.parent=No parent directory found for ''{0}'' | |
| 196 | Main.status.export.concat.extension=File name must have an extension ''{0}'' | |
| 197 | Main.status.export.concat.io=Could not read from ''{0}'' | |
| 198 | ||
| 199 | Main.status.typeset.create=Creating typesetter | |
| 200 | Main.status.typeset.xhtml=Export document as XHTML | |
| 201 | Main.status.typeset.began=Started typesetting ''{0}'' | |
| 202 | Main.status.typeset.failed=Could not generate PDF file | |
| 203 | Main.status.typeset.page=Typesetting page {0} of {1} (pass {2}) | |
| 204 | Main.status.typeset.ended.success=Finished typesetting ''{0}'' ({1} elapsed) | |
| 205 | Main.status.typeset.ended.failure=Failed to typeset ''{0}'' ({1} elapsed) | |
| 206 | Main.status.typeset.setting=Set {0} to ''{1}'' | |
| 207 | ||
| 208 | Main.status.lexicon.loading=Loading lexicon: {0} words | |
| 209 | Main.status.lexicon.loaded=Loaded lexicon: {0} words | |
| 210 | ||
| 211 | # ######################################################################## | |
| 212 | # Search Bar | |
| 213 | # ######################################################################## | |
| 214 | ||
| 215 | Main.search.stop.tooltip=Close search bar | |
| 216 | Main.search.stop.icon=CLOSE | |
| 217 | Main.search.next.tooltip=Find next match | |
| 218 | Main.search.next.icon=CHEVRON_DOWN | |
| 219 | Main.search.prev.tooltip=Find previous match | |
| 220 | Main.search.prev.icon=CHEVRON_UP | |
| 221 | Main.search.find.tooltip=Search document for text | |
| 222 | Main.search.find.icon=SEARCH | |
| 223 | Main.search.match.none=No matches | |
| 224 | Main.search.match.some={0} of {1} matches | |
| 225 | ||
| 226 | # ######################################################################## | |
| 227 | # Definition Pane and its Tree View | |
| 228 | # ######################################################################## | |
| 229 | ||
| 230 | Definition.menu.add.default=Undefined | |
| 231 | ||
| 232 | # ######################################################################## | |
| 233 | # Variable Definitions Pane | |
| 234 | # ######################################################################## | |
| 235 | ||
| 236 | Pane.definition.node.root.title=Variables | |
| 237 | ||
| 238 | # ######################################################################## | |
| 239 | # HTML Preview Pane | |
| 240 | # ######################################################################## | |
| 241 | ||
| 242 | Pane.preview.title=Preview | |
| 243 | ||
| 244 | # ######################################################################## | |
| 245 | # Document Outline Pane | |
| 246 | # ######################################################################## | |
| 247 | ||
| 248 | Pane.outline.title=Outline | |
| 249 | ||
| 250 | # ######################################################################## | |
| 251 | # File Manager Pane | |
| 252 | # ######################################################################## | |
| 253 | ||
| 254 | Pane.files.title=Files | |
| 255 | ||
| 256 | # ######################################################################## | |
| 257 | # Document Outline Pane | |
| 258 | # ######################################################################## | |
| 259 | ||
| 260 | Pane.statistics.title=Statistics | |
| 261 | ||
| 262 | # ######################################################################## | |
| 263 | # Failure messages with respect to YAML files. | |
| 264 | # ######################################################################## | |
| 265 | ||
| 266 | yaml.error.open=Could not open YAML file (ensure non-empty file). | |
| 267 | yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''. | |
| 268 | yaml.error.missing=Empty variable value for key ''{0}''. | |
| 269 | yaml.error.tree.form=Unassigned variable near ''{0}''. | |
| 270 | ||
| 271 | # ######################################################################## | |
| 272 | # Text Resource | |
| 273 | # ######################################################################## | |
| 274 | ||
| 275 | TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist. | |
| 276 | TextResource.load.error.permissions=The file ''{0}'' must be readable and writable. | |
| 277 | ||
| 278 | TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1} | |
| 279 | TextResource.saveFailed.title=Save | |
| 280 | ||
| 281 | # ######################################################################## | |
| 282 | # File Open | |
| 283 | # ######################################################################## | |
| 284 | ||
| 285 | Dialog.file.choose.open.title=Open File | |
| 286 | Dialog.file.choose.save.title=Save File | |
| 287 | Dialog.file.choose.export.title=Export File | |
| 288 | Dialog.file.choose.import.title=Import File | |
| 289 | ||
| 290 | Dialog.file.choose.filter.title.source=Source Files | |
| 291 | Dialog.file.choose.filter.title.definition=Variable Files | |
| 292 | Dialog.file.choose.filter.title.xml=XML Files | |
| 293 | Dialog.file.choose.filter.title.all=All Files | |
| 294 | ||
| 295 | # ######################################################################## | |
| 296 | # Browse File | |
| 297 | # ######################################################################## | |
| 298 | ||
| 299 | BrowseFileButton.chooser.title=Open local file | |
| 300 | BrowseFileButton.chooser.allFilesFilter=All Files | |
| 301 | BrowseFileButton.tooltip=${BrowseFileButton.chooser.title} | |
| 302 | ||
| 303 | # ######################################################################## | |
| 304 | # Browse Directory | |
| 305 | # ######################################################################## | |
| 306 | ||
| 307 | BrowseDirectoryButton.chooser.title=Open local directory | |
| 308 | BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title} | |
| 309 | ||
| 310 | # ######################################################################## | |
| 311 | # Alert Dialog | |
| 312 | # ######################################################################## | |
| 313 | ||
| 314 | Alert.file.close.title=Close | |
| 315 | Alert.file.close.text=Save changes to {0}? | |
| 316 | ||
| 317 | # ######################################################################## | |
| 318 | # Typesetter Installation Wizard | |
| 319 | # ######################################################################## | |
| 320 | ||
| 321 | Wizard.typesetter.name=ConTeXt | |
| 322 | Wizard.typesetter.container.name=Podman | |
| 323 | Wizard.typesetter.container.version=4.6.2 | |
| 324 | Wizard.typesetter.container.checksum=a51acef00b17cce83dd4d364817af32dd5e541db8d2d13063ae73742744ba3ad | |
| 325 | Wizard.typesetter.container.image.name=typesetter | |
| 326 | Wizard.typesetter.container.image.version=3.0.1 | |
| 327 | Wizard.typesetter.container.image.tag=${Wizard.typesetter.container.image.name}:${Wizard.typesetter.container.image.version} | |
| 328 | Wizard.typesetter.container.image.url=https://repository.keenwrite.com/containers/${Wizard.typesetter.container.image.tag} | |
| 329 | Wizard.typesetter.themes.version=1.9.1 | |
| 330 | Wizard.typesetter.themes.checksum=c6411a92d660e2f2fe608dac0dba13d2d0f5b4b25b88f19db79eda91b36b3b4c | |
| 14 | workspace.document.meta.desc=Keys must be alphabetic, values may use variables (e.g., '{{'document.title'}}'). | |
| 15 | workspace.document.meta.title=Pairs | |
| 16 | ||
| 17 | workspace.editor=Editor | |
| 18 | workspace.editor.autosave=Autosave | |
| 19 | workspace.editor.autosave.desc=Amount of time to wait between saves, in seconds (0 means disabled). | |
| 20 | workspace.editor.autosave.title=Timeout | |
| 21 | ||
| 22 | workspace.typeset=Typesetting | |
| 23 | workspace.typeset.context=ConTeXt | |
| 24 | workspace.typeset.context.themes.path=Paths | |
| 25 | workspace.typeset.context.themes.path.desc=Directory containing theme subdirectories. | |
| 26 | workspace.typeset.context.themes.path.title=Themes | |
| 27 | workspace.typeset.context.clean=Clean | |
| 28 | workspace.typeset.context.clean.desc=Delete ancillary files after an unsuccessful export. | |
| 29 | workspace.typeset.context.clean.title=Purge | |
| 30 | workspace.typeset.context.fonts=Fonts | |
| 31 | workspace.typeset.context.fonts.dir=Directory | |
| 32 | workspace.typeset.context.fonts.dir.desc=Directory containing additional font files (OTF and TTF). | |
| 33 | workspace.typeset.context.fonts.dir.title=Path | |
| 34 | workspace.typeset.typography=Typography | |
| 35 | workspace.typeset.typography.quotes=Quotation Marks | |
| 36 | workspace.typeset.typography.quotes.desc=Export straight quotes and apostrophes as curled equivalents. | |
| 37 | workspace.typeset.typography.quotes.title=Curl | |
| 38 | ||
| 39 | workspace.r=R | |
| 40 | workspace.r.script=Startup Script | |
| 41 | workspace.r.script.desc=Script runs prior to executing R statements within the document. | |
| 42 | workspace.r.dir=Working Directory | |
| 43 | workspace.r.dir.desc=Value assigned to v$application$r$working$directory and usable in the startup script. | |
| 44 | workspace.r.dir.title=Directory | |
| 45 | workspace.r.delimiter.began=Delimiter Prefix | |
| 46 | workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted variables. | |
| 47 | workspace.r.delimiter.began.title=Opening | |
| 48 | workspace.r.delimiter.ended=Delimiter Suffix | |
| 49 | workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted variables. | |
| 50 | workspace.r.delimiter.ended.title=Closing | |
| 51 | ||
| 52 | workspace.images=Images | |
| 53 | workspace.images.dir=Absolute Directory | |
| 54 | workspace.images.dir.desc=Path to search for local file system images. | |
| 55 | workspace.images.dir.title=Directory | |
| 56 | workspace.images.cache.desc=Path to store remotely retrieved images. | |
| 57 | workspace.images.cache.title=Directory | |
| 58 | workspace.images.order=Extensions | |
| 59 | workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces. | |
| 60 | workspace.images.order.title=Extensions | |
| 61 | workspace.images.resize=Resize | |
| 62 | workspace.images.resize.desc=Scale images to fit the preview panel when resizing, automatically. | |
| 63 | workspace.images.resize.title=Resize | |
| 64 | workspace.images.server=Diagram Server | |
| 65 | workspace.images.server.desc=Server used to generate diagrams (e.g., kroki.io). | |
| 66 | workspace.images.server.title=Name | |
| 67 | ||
| 68 | workspace.definition=Variable | |
| 69 | workspace.definition.path=File name | |
| 70 | workspace.definition.path.desc=Absolute path to interpolated string variables. | |
| 71 | workspace.definition.path.title=Path | |
| 72 | workspace.definition.delimiter.began=Delimiter Prefix | |
| 73 | workspace.definition.delimiter.began.desc=Indicates when a variable name is starting. | |
| 74 | workspace.definition.delimiter.began.title=Opening | |
| 75 | workspace.definition.delimiter.ended=Delimiter Suffix | |
| 76 | workspace.definition.delimiter.ended.desc=Indicates when a variable name is ending. | |
| 77 | workspace.definition.delimiter.ended.title=Closing | |
| 78 | ||
| 79 | workspace.ui.skin=Skins | |
| 80 | workspace.ui.skin.selection=Bundled | |
| 81 | workspace.ui.skin.selection.desc=Pre-packaged application style (default: Modena Light). | |
| 82 | workspace.ui.skin.selection.title=Name | |
| 83 | workspace.ui.skin.custom=Custom | |
| 84 | workspace.ui.skin.custom.desc=User-defined JavaFX cascading stylesheet file. | |
| 85 | workspace.ui.skin.custom.title=Path | |
| 86 | ||
| 87 | workspace.ui.preview=Preview | |
| 88 | workspace.ui.preview.stylesheet=Stylesheet | |
| 89 | workspace.ui.preview.stylesheet.desc=User-defined HTML cascading stylesheet file. | |
| 90 | workspace.ui.preview.stylesheet.title=Path | |
| 91 | ||
| 92 | workspace.ui.font=Fonts | |
| 93 | workspace.ui.font.editor=Editor Font | |
| 94 | workspace.ui.font.editor.name=Name | |
| 95 | workspace.ui.font.editor.name.desc=Text editor font name (sans-serif font recommended). | |
| 96 | workspace.ui.font.editor.name.title=Family | |
| 97 | workspace.ui.font.editor.size=Size | |
| 98 | workspace.ui.font.editor.size.desc=Font size. | |
| 99 | workspace.ui.font.editor.size.title=Points | |
| 100 | workspace.ui.font.preview=Preview Font | |
| 101 | workspace.ui.font.preview.name=Name | |
| 102 | workspace.ui.font.preview.name.desc=Preview pane font name (must support ligatures, serif font recommended). | |
| 103 | workspace.ui.font.preview.name.title=Family | |
| 104 | workspace.ui.font.preview.size=Size | |
| 105 | workspace.ui.font.preview.size.desc=Font size. | |
| 106 | workspace.ui.font.preview.size.title=Points | |
| 107 | workspace.ui.font.preview.mono.name=Name | |
| 108 | workspace.ui.font.preview.mono.name.desc=Monospace font name. | |
| 109 | workspace.ui.font.preview.mono.name.title=Family | |
| 110 | workspace.ui.font.preview.mono.size=Size | |
| 111 | workspace.ui.font.preview.mono.size.desc=Monospace font size. | |
| 112 | workspace.ui.font.preview.mono.size.title=Points | |
| 113 | workspace.ui.font.math=Math Font | |
| 114 | workspace.ui.font.math.size.title=Scale | |
| 115 | ||
| 116 | workspace.language=Language | |
| 117 | workspace.language.locale=Internationalization | |
| 118 | workspace.language.locale.desc=Language for application and HTML export. | |
| 119 | workspace.language.locale.title=Locale | |
| 120 | ||
| 121 | # ######################################################################## | |
| 122 | # Editor actions | |
| 123 | # ######################################################################## | |
| 124 | ||
| 125 | Editor.spelling.check.matches.none=No suggestions for ''{0}'' found. | |
| 126 | Editor.spelling.check.matches.okay=The spelling for ''{0}'' appears to be correct. | |
| 127 | ||
| 128 | # ######################################################################## | |
| 129 | # Menu Bar | |
| 130 | # ######################################################################## | |
| 131 | ||
| 132 | Main.menu.file=_File | |
| 133 | Main.menu.edit=_Edit | |
| 134 | Main.menu.insert=_Insert | |
| 135 | Main.menu.format=Forma_t | |
| 136 | Main.menu.definition=_Variable | |
| 137 | Main.menu.view=Vie_w | |
| 138 | Main.menu.help=_Help | |
| 139 | ||
| 140 | # ######################################################################## | |
| 141 | # Detachable Tabs | |
| 142 | # ######################################################################## | |
| 143 | ||
| 144 | # {0} is the application title; {1} is a unique window ID. | |
| 145 | Detach.tab.title={0} - {1} | |
| 146 | ||
| 147 | # ######################################################################## | |
| 148 | # Status Bar | |
| 149 | # ######################################################################## | |
| 150 | ||
| 151 | Main.status.text.offset=offset | |
| 152 | Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2} | |
| 153 | Main.status.state.default=OK | |
| 154 | Main.status.export.success=Saved as ''{0}'' | |
| 155 | ||
| 156 | Main.status.error.bootstrap.eval=Note: Bootstrap variable of ''{0}'' not found | |
| 157 | Main.status.error.bootstrap.cache=Could not create cache directory ''{0}'' | |
| 158 | ||
| 159 | Main.status.error.parse=Evaluation error: {0} | |
| 160 | Main.status.error.def.blank=Move the caret to a word before inserting a variable | |
| 161 | Main.status.error.def.empty=Create a variable before inserting one | |
| 162 | Main.status.error.def.missing=No variable value found for ''{0}'' | |
| 163 | Main.status.error.r=Error with [{0}...]: {1} | |
| 164 | ||
| 165 | Main.status.error.file.missing=Not found: ''{0}'' | |
| 166 | Main.status.error.file.missing.near=Not found: ''{0}'' near line {1} | |
| 167 | Main.status.error.file.delete=Failed to delete ''{0}'' | |
| 168 | ||
| 169 | Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}'' | |
| 170 | Main.status.error.messages.syntax=Missing ''}'' in ''{0}'' | |
| 171 | ||
| 172 | Main.status.error.undo=Cannot undo; beginning of undo history reached | |
| 173 | Main.status.error.redo=Cannot redo; end of redo history reached | |
| 174 | ||
| 175 | Main.status.error.theme.missing=Install themes before exporting (no themes found at ''{0}'') | |
| 176 | Main.status.error.theme.name=Cannot find theme name for ''{0}'' | |
| 177 | ||
| 178 | Main.status.image.request.init=Initializing HTTP request | |
| 179 | Main.status.image.request.fetch=Downloaded image ''{0}'' | |
| 180 | Main.status.image.request.success=Determined content type ''{0}'' | |
| 181 | Main.status.image.request.error.media=No media type for ''{0}'' | |
| 182 | Main.status.image.request.error.cert=Could not accept certificate for ''{0}'' | |
| 183 | Main.status.image.request.error.rasterize=Rasterizer could not parse SVG image | |
| 184 | ||
| 185 | Main.status.image.xhtml.image.download=Downloading ''{0}'' | |
| 186 | Main.status.image.xhtml.image.resolve=Qualify path for ''{0}'' | |
| 187 | Main.status.image.xhtml.image.found=Found image ''{0}'' | |
| 188 | Main.status.image.xhtml.image.missing=Missing image ''{0}'' | |
| 189 | Main.status.image.xhtml.image.saved=Saved image ''{0}'' | |
| 190 | Main.status.image.xhtml.image.failed=Cannot save image ''{0}'' | |
| 191 | ||
| 192 | Main.status.font.search.missing=No font name starting with ''{0}'' was found | |
| 193 | ||
| 194 | Main.status.export.concat=Concatenating ''{0}'' | |
| 195 | Main.status.export.concat.parent=No parent directory found for ''{0}'' | |
| 196 | Main.status.export.concat.extension=File name must have an extension ''{0}'' | |
| 197 | Main.status.export.concat.io=Could not read from ''{0}'' | |
| 198 | ||
| 199 | Main.status.typeset.create=Creating typesetter | |
| 200 | Main.status.typeset.xhtml=Export document as XHTML | |
| 201 | Main.status.typeset.began=Started typesetting ''{0}'' | |
| 202 | Main.status.typeset.failed=Could not generate PDF file | |
| 203 | Main.status.typeset.page=Typesetting page {0} of {1} (pass {2}) | |
| 204 | Main.status.typeset.ended.success=Finished typesetting ''{0}'' ({1} elapsed) | |
| 205 | Main.status.typeset.ended.failure=Failed to typeset ''{0}'' ({1} elapsed) | |
| 206 | Main.status.typeset.setting=Set {0} to ''{1}'' | |
| 207 | ||
| 208 | Main.status.lexicon.loading=Loading lexicon: {0} words | |
| 209 | Main.status.lexicon.loaded=Loaded lexicon: {0} words | |
| 210 | ||
| 211 | # ######################################################################## | |
| 212 | # Search Bar | |
| 213 | # ######################################################################## | |
| 214 | ||
| 215 | Main.search.stop.tooltip=Close search bar | |
| 216 | Main.search.stop.icon=CLOSE | |
| 217 | Main.search.next.tooltip=Find next match | |
| 218 | Main.search.next.icon=CHEVRON_DOWN | |
| 219 | Main.search.prev.tooltip=Find previous match | |
| 220 | Main.search.prev.icon=CHEVRON_UP | |
| 221 | Main.search.find.tooltip=Search document for text | |
| 222 | Main.search.find.icon=SEARCH | |
| 223 | Main.search.match.none=No matches | |
| 224 | Main.search.match.some={0} of {1} matches | |
| 225 | ||
| 226 | # ######################################################################## | |
| 227 | # Definition Pane and its Tree View | |
| 228 | # ######################################################################## | |
| 229 | ||
| 230 | Definition.menu.add.default=Undefined | |
| 231 | ||
| 232 | # ######################################################################## | |
| 233 | # Variable Definitions Pane | |
| 234 | # ######################################################################## | |
| 235 | ||
| 236 | Pane.definition.node.root.title=Variables | |
| 237 | ||
| 238 | # ######################################################################## | |
| 239 | # HTML Preview Pane | |
| 240 | # ######################################################################## | |
| 241 | ||
| 242 | Pane.preview.title=Preview | |
| 243 | ||
| 244 | # ######################################################################## | |
| 245 | # Document Outline Pane | |
| 246 | # ######################################################################## | |
| 247 | ||
| 248 | Pane.outline.title=Outline | |
| 249 | ||
| 250 | # ######################################################################## | |
| 251 | # File Manager Pane | |
| 252 | # ######################################################################## | |
| 253 | ||
| 254 | Pane.files.title=Files | |
| 255 | ||
| 256 | # ######################################################################## | |
| 257 | # Document Outline Pane | |
| 258 | # ######################################################################## | |
| 259 | ||
| 260 | Pane.statistics.title=Statistics | |
| 261 | ||
| 262 | # ######################################################################## | |
| 263 | # Failure messages with respect to YAML files. | |
| 264 | # ######################################################################## | |
| 265 | ||
| 266 | yaml.error.open=Could not open YAML file (ensure non-empty file). | |
| 267 | yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''. | |
| 268 | yaml.error.missing=Empty variable value for key ''{0}''. | |
| 269 | yaml.error.tree.form=Unassigned variable near ''{0}''. | |
| 270 | ||
| 271 | # ######################################################################## | |
| 272 | # Text Resource | |
| 273 | # ######################################################################## | |
| 274 | ||
| 275 | TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist. | |
| 276 | TextResource.load.error.permissions=The file ''{0}'' must be readable and writable. | |
| 277 | ||
| 278 | TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1} | |
| 279 | TextResource.saveFailed.title=Save | |
| 280 | ||
| 281 | # ######################################################################## | |
| 282 | # File Open | |
| 283 | # ######################################################################## | |
| 284 | ||
| 285 | Dialog.file.choose.open.title=Open File | |
| 286 | Dialog.file.choose.save.title=Save File | |
| 287 | Dialog.file.choose.export.title=Export File | |
| 288 | Dialog.file.choose.import.title=Import File | |
| 289 | ||
| 290 | Dialog.file.choose.filter.title.source=Source Files | |
| 291 | Dialog.file.choose.filter.title.definition=Variable Files | |
| 292 | Dialog.file.choose.filter.title.xml=XML Files | |
| 293 | Dialog.file.choose.filter.title.all=All Files | |
| 294 | ||
| 295 | # ######################################################################## | |
| 296 | # Browse File | |
| 297 | # ######################################################################## | |
| 298 | ||
| 299 | BrowseFileButton.chooser.title=Open local file | |
| 300 | BrowseFileButton.chooser.allFilesFilter=All Files | |
| 301 | BrowseFileButton.tooltip=${BrowseFileButton.chooser.title} | |
| 302 | ||
| 303 | # ######################################################################## | |
| 304 | # Browse Directory | |
| 305 | # ######################################################################## | |
| 306 | ||
| 307 | BrowseDirectoryButton.chooser.title=Open local directory | |
| 308 | BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title} | |
| 309 | ||
| 310 | # ######################################################################## | |
| 311 | # Alert Dialog | |
| 312 | # ######################################################################## | |
| 313 | ||
| 314 | Alert.file.close.title=Close | |
| 315 | Alert.file.close.text=Save changes to {0}? | |
| 316 | ||
| 317 | # ######################################################################## | |
| 318 | # Typesetter Installation Wizard | |
| 319 | # ######################################################################## | |
| 320 | ||
| 321 | Wizard.typesetter.name=ConTeXt | |
| 322 | Wizard.typesetter.container.name=Podman | |
| 323 | Wizard.typesetter.container.version=4.8.2 | |
| 324 | Wizard.typesetter.container.checksum=250b12c24444005e09306eda38fa63c60cb1bdadf040f4e3f24f976e213cd462 | |
| 325 | Wizard.typesetter.container.image.name=typesetter | |
| 326 | Wizard.typesetter.container.image.version=3.1.0 | |
| 327 | Wizard.typesetter.container.image.tag=${Wizard.typesetter.container.image.name}:${Wizard.typesetter.container.image.version} | |
| 328 | Wizard.typesetter.container.image.url=https://repository.keenwrite.com/containers/${Wizard.typesetter.container.image.tag} | |
| 329 | Wizard.typesetter.themes.version=1.10.0 | |
| 330 | Wizard.typesetter.themes.checksum=38ce9c130cb8f527465baa3ca1e79c23ff92156c4fe9b842cc04fd80a7e10359 | |
| 331 | 331 | |
| 332 | 332 | Wizard.container.install.command=Installing container using: ''{0}'' |
| 49 | 49 | # discerned so that the correct type of variable |
| 50 | 50 | # reference can be inserted. |
| 51 | file.default.document=untitled.md | |
| 51 | file.default.document.prefix=untitled | |
| 52 | file.default.document.suffix=md | |
| 53 | file.default.document=${file.default.document.prefix}.${file.default.document.suffix} | |
| 52 | 54 | file.default.definition=variables.yaml |
| 53 | 55 |