Dave Jarvis' Repositories

M README.md
7979
Typesetting to PDF files requires the following:
8080
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)
8282
* [ConTeXt](https://wiki.contextgarden.net/Installation)
8383
M README.zh-CN.md
7373
排版到 PDF 文件需要以下內容:
7474
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)
7676
* [ConTeXt](https://wiki.contextgarden.net/Installation)
7777
M build.gradle
2323
spotbugs {
2424
  excludeFilter.set(
25
      file( "${projectDir}/bug-filter.xml" )
25
    file( "${projectDir}/bug-filter.xml" )
2626
  )
2727
}
...
6262
6363
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',
7777
]
7878
...
9191
  def v_junit = '5.10.1'
9292
  def v_flexmark = '0.64.8'
93
  def v_jackson = '2.15.3'
93
  def v_jackson = '2.16.0'
9494
  def v_echosvg = '1.0.1'
9595
  def v_picocli = '4.7.5'
9696
9797
  // JavaFX
9898
  implementation 'org.controlsfx:controlsfx:11.2.0'
9999
  implementation 'org.fxmisc.richtext:richtextfx:0.11.2'
100100
  implementation 'org.fxmisc.flowless:flowless:0.7.2'
101101
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
102
  implementation 'com.miglayout:miglayout-javafx:11.2'
102
  implementation 'com.miglayout:miglayout-javafx:11.3'
103103
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.16.0'
104104
  implementation 'com.panemu:tiwulfx-dock:0.2'
...
209209
compileJava {
210210
  options.compilerArgs += [
211
      "-Xlint:unchecked",
212
      "-Xlint:deprecation",
213
      "-Aproject=${applicationPackage}/${applicationName}"
211
    "-Xlint:unchecked",
212
    "-Xlint:deprecation",
213
    "-Aproject=${applicationPackage}/${applicationName}"
214214
  ]
215215
}
...
236236
  from {
237237
    (configurations.runtimeClasspath.findAll { !it.path.endsWith( ".pom" ) })
238
        .collect { it.isDirectory() ? it : zipTree( it ) }
238
      .collect { it.isDirectory() ? it : zipTree( it ) }
239239
  }
240240
M container/Containerfile
3131
WORKDIR $DOWNLOAD_DIR
3232
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
4435
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" ./
5553
5654
# Typesetting software
...
6866
    add ca-certificates curl fontconfig inkscape rsync && \
6967
  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" && \
7275
  echo "export CONTEXT_HOME=\"$CONTEXT_HOME\"" >> $PROFILE && \
7376
  echo "export PATH=\"\$PATH:\$CONTEXT_HOME/tex/texmf-linuxmusl/bin\"" >> $PROFILE && \
7477
  echo "export OSFONTDIR=\"/usr/share/fonts//\"" >> $PROFILE && \
7578
  echo "PS1='\\u@typesetter:\\w\\$ '" >> $PROFILE && \
7679
  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" && \
7883
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/courier-prime.zip "*.ttf" && \
7984
  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" && \
8186
  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" && \
8392
  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" && \
8796
  fc-cache -f -v && \
8897
  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 && \
9698
  sh install.sh && \
97
  rm -f $DOWNLOAD_DIR/*.zip && \
9899
  rm -rf \
99100
    "modules" \
100101
    "/var/cache" \
101102
    "/usr/share/icons" \
103
    "/opt/context/tex/texmf-context/source" \
104
    $DOWNLOAD_DIR/*.zip \
102105
    $CONTEXT_HOME/tex/texmf-modules/doc \
103106
    $CONTEXT_HOME/tex/texmf-context/doc && \
M container/README.md
22
33
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.
66
77
## Installation wizard
88
99
The installation wizard is responsible for installing the containerization
1010
software and the container image. The container manager class loads the
1111
image from a URL. That URL is defined in the `messages.properties` file.
1212
1313
# Upgrade
1414
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' '
1629
1730
1. Edit `src/main/resources/com/keenwrite/messages.properties`.
1831
1. Set `Wizard.typesetter.container.version` to the latest version.
1932
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.
2134
1. Save the file.
2235
23
The containerization software versions are changed.
36
The containerization software version is changed.
2437
2538
# Publish
2639
2740
Publish the changes to the container image as follows:
2841
2942
``` bash
30
./manage.sh --build
31
./manage.sh --export
32
./manage.sh --publish
43
./manage.sh --delete --build --export --publish
3344
```
3445
M container/manage.sh
9191
# ---------------------------------------------------------------------------
9292
utile_build() {
93
  $log "Building"
93
  $log "Building container version ${CONTAINER_VERSION}"
9494
9595
  # Show what commands are run while building, but not the commands' output.
M docs/references.md
6868
* `[@type-name:label]` (reference)
6969
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.
7373
7474
Consider the following example:
M libs/keenquotes.jar
Binary file
M src/main/java/com/keenwrite/MainPane.java
22
package com.keenwrite;
33
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 );
501514
  }
502515
M src/main/java/com/keenwrite/constants/Constants.java
259259
  }
260260
261
  static String get( final String key ) {
261
  public static String get( final String key ) {
262262
    return sSettings.getSetting( key, "" );
263263
  }
...
270270
   */
271271
  private static File getFile( final String suffix ) {
272
    return new File( get( "file.default." + suffix ) );
272
    return new File( get( STR."file.default.\{suffix}" ) );
273273
  }
274274
M src/main/java/com/keenwrite/events/StatusEvent.java
6767
   */
6868
  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 );
7171
    final var trace = mProblem;
7272
7373
    if( trace != null ) {
7474
      stream( trace.getStackTrace() )
75
        .takeWhile( StatusEvent::filter )
76
        .limit( 15 )
75
        .limit( 150 )
7776
        .toList()
7877
        .forEach( e -> sb.append( e.toString() ).append( NEWLINE ) );
...
9190
                   message.isBlank() ? "" : " ",
9291
                   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
    );
10892
  }
10993
M src/main/java/com/keenwrite/ui/actions/Action.java
4848
     */
4949
    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";
5454
      final var builder = setText( text ).setIcon( icon );
5555
...
172172
173173
    if( mAccelerator != null ) {
174
      tooltip += " (" + mAccelerator.getDisplayText() + ')';
174
      tooltip += STR." (\{mAccelerator.getDisplayText()}\{')'}";
175175
    }
176176
M src/main/java/com/keenwrite/ui/actions/ApplicationBars.java
5959
    return createMenu(
6060
      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() ),
6363
      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() ),
6666
      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() ),
7070
      SEPARATOR,
71
      addAction( "file.export", e -> { } )
71
      addAction( "file.export", _ -> { } )
7272
        .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() )
8080
        ),
8181
      SEPARATOR,
82
      addAction( "file.exit", e -> actions.file_exit() )
82
      addAction( "file.exit", _ -> actions.file_exit() )
8383
    );
8484
    // @formatter:on
...
9191
      get( "Main.menu.edit" ),
9292
      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() ),
9595
      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() ),
100100
      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() ),
104104
      SEPARATOR,
105
      addAction( "edit.preferences", e -> actions.edit_preferences() )
105
      addAction( "edit.preferences", _ -> actions.edit_preferences() )
106106
    );
107107
  }
108108
109109
  @NotNull
110110
  private static Menu createMenuFormat( final GuiCommands actions ) {
111111
    return createMenu(
112112
      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() )
119119
    );
120120
  }
...
127127
    return createMenu(
128128
      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() ),
132132
      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() ),
135135
      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() ),
139139
      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() )
143143
    );
144144
    // @formatter:on
145145
  }
146146
147147
  @NotNull
148148
  private static Menu createMenuVariable(
149149
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
150150
    return createMenu(
151151
      get( "Main.menu.definition" ),
152
      addAction( "definition.insert", e -> actions.definition_autoinsert() ),
152
      addAction( "definition.insert", _ -> actions.definition_autoinsert() ),
153153
      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() )
157157
    );
158158
  }
159159
160160
  @NotNull
161161
  private static Menu createMenuView(
162162
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
163163
    return createMenu(
164164
      get( "Main.menu.view" ),
165
      addAction( "view.refresh", e -> actions.view_refresh() ),
165
      addAction( "view.refresh", _ -> actions.view_refresh() ),
166166
      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() ),
171171
      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() ),
175175
      SEPARATOR,
176
      addAction( "view.log", e -> actions.view_log() )
176
      addAction( "view.log", _ -> actions.view_log() )
177177
    );
178178
  }
179179
180180
  @NotNull
181181
  private static Menu createMenuHelp( final GuiCommands actions ) {
182182
    return createMenu(
183183
      get( "Main.menu.help" ),
184
      addAction( "help.about", e -> actions.help_about() )
184
      addAction( "help.about", _ -> actions.help_about() )
185185
    );
186186
  }
M src/main/resources/com/keenwrite/messages.properties
1212
1313
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
331331
332332
Wizard.container.install.command=Installing container using: ''{0}''
M src/main/resources/com/keenwrite/settings.properties
4949
# discerned so that the correct type of variable
5050
# 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}
5254
file.default.definition=variables.yaml
5355