Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
docs/references.md
* `[@type-name:label]` (reference)
-The `type-name` can be any alphanumeric value, starting with a letter.
-Type names are user-defined categories for the item type. Labels are
-user-defined identifiers that must be unique per item.
+The `type-name` can be any alphanumeric value, starting with a letter or
+non-numeric ideogram. Type names are user-defined categories for the item
+type. Labels are user-defined identifiers that must be unique per item.
Consider the following example:
src/main/java/com/keenwrite/MainPane.java
package com.keenwrite;
-import com.keenwrite.editors.TextDefinition;
-import com.keenwrite.editors.TextEditor;
-import com.keenwrite.editors.TextResource;
-import com.keenwrite.editors.common.ScrollEventHandler;
-import com.keenwrite.editors.common.VariableNameInjector;
-import com.keenwrite.editors.definition.DefinitionEditor;
-import com.keenwrite.editors.definition.TreeTransformer;
-import com.keenwrite.editors.definition.yaml.YamlTreeTransformer;
-import com.keenwrite.editors.markdown.MarkdownEditor;
-import com.keenwrite.events.*;
-import com.keenwrite.events.spelling.LexiconLoadedEvent;
-import com.keenwrite.io.MediaType;
-import com.keenwrite.io.MediaTypeExtension;
-import com.keenwrite.preferences.Workspace;
-import com.keenwrite.preview.HtmlPreview;
-import com.keenwrite.processors.HtmlPreviewProcessor;
-import com.keenwrite.processors.Processor;
-import com.keenwrite.processors.ProcessorContext;
-import com.keenwrite.processors.ProcessorFactory;
-import com.keenwrite.processors.r.Engine;
-import com.keenwrite.processors.r.RBootstrapController;
-import com.keenwrite.service.events.Notifier;
-import com.keenwrite.spelling.api.SpellChecker;
-import com.keenwrite.spelling.impl.PermissiveSpeller;
-import com.keenwrite.spelling.impl.SymSpellSpeller;
-import com.keenwrite.typesetting.installer.TypesetterInstaller;
-import com.keenwrite.ui.explorer.FilePickerFactory;
-import com.keenwrite.ui.heuristics.DocumentStatistics;
-import com.keenwrite.ui.outline.DocumentOutline;
-import com.keenwrite.ui.spelling.TextEditorSpellChecker;
-import com.keenwrite.util.GenericBuilder;
-import com.panemu.tiwulfx.control.dock.DetachableTab;
-import com.panemu.tiwulfx.control.dock.DetachableTabPane;
-import javafx.beans.property.*;
-import javafx.collections.ListChangeListener;
-import javafx.concurrent.Task;
-import javafx.event.ActionEvent;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.SplitPane;
-import javafx.scene.control.Tab;
-import javafx.scene.control.TabPane;
-import javafx.scene.control.Tooltip;
-import javafx.scene.control.TreeItem.TreeModificationEvent;
-import javafx.scene.input.KeyEvent;
-import javafx.stage.Stage;
-import javafx.stage.Window;
-import org.greenrobot.eventbus.Subscribe;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.nio.file.Path;
-import java.util.*;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import static com.keenwrite.ExportFormat.NONE;
-import static com.keenwrite.Launcher.terminate;
-import static com.keenwrite.Messages.get;
-import static com.keenwrite.constants.Constants.*;
-import static com.keenwrite.events.Bus.register;
-import static com.keenwrite.events.StatusEvent.clue;
-import static com.keenwrite.io.MediaType.*;
-import static com.keenwrite.io.MediaType.TypeName.TEXT;
-import static com.keenwrite.io.SysFile.toFile;
-import static com.keenwrite.preferences.AppKeys.*;
-import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
-import static com.keenwrite.processors.ProcessorContext.Mutator;
-import static com.keenwrite.processors.ProcessorContext.builder;
-import static com.keenwrite.processors.ProcessorFactory.createProcessors;
-import static java.awt.Desktop.getDesktop;
-import static java.util.concurrent.Executors.newFixedThreadPool;
-import static java.util.concurrent.Executors.newScheduledThreadPool;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static java.util.stream.Collectors.groupingBy;
-import static javafx.application.Platform.exit;
-import static javafx.application.Platform.runLater;
-import static javafx.scene.control.ButtonType.NO;
-import static javafx.scene.control.ButtonType.YES;
-import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCode.SPACE;
-import static javafx.scene.input.KeyCombination.ALT_DOWN;
-import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
-import static javafx.util.Duration.millis;
-import static javax.swing.SwingUtilities.invokeLater;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-
-/**
- * Responsible for wiring together the main application components for a
- * particular {@link Workspace} (project). These include the definition views,
- * text editors, and preview pane along with any corresponding controllers.
- */
-public final class MainPane extends SplitPane {
-
- private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
- private static final Notifier sNotifier = Services.load( Notifier.class );
-
- /**
- * Used when opening files to determine how each file should be binned and
- * therefore what tab pane to be opened within.
- */
- private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
- TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED
- );
-
- private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 );
- private final AtomicReference<ScheduledFuture<?>> mSaveTask =
- new AtomicReference<>();
-
- /**
- * Prevents re-instantiation of processing classes.
- */
- private final Map<TextResource, Processor<String>> mProcessors =
- new HashMap<>();
-
- private final Workspace mWorkspace;
-
- /**
- * Groups similar file type tabs together.
- */
- private final List<TabPane> mTabPanes = new ArrayList<>();
-
- /**
- * Renders the actively selected plain text editor tab.
- */
- private final HtmlPreview mPreview;
-
- /**
- * Provides an interactive document outline.
- */
- private final DocumentOutline mOutline = new DocumentOutline();
-
- /**
- * Changing the active editor fires the value changed event. This allows
- * refreshes to happen when external definitions are modified and need to
- * trigger the processing chain.
- */
- private final ObjectProperty<TextEditor> mTextEditor =
- new SimpleObjectProperty<>();
-
- /**
- * Changing the active definition editor fires the value changed event. This
- * allows refreshes to happen when external definitions are modified and need
- * to trigger the processing chain.
- */
- private final ObjectProperty<TextDefinition> mDefinitionEditor =
- new SimpleObjectProperty<>();
-
- private final ObjectProperty<SpellChecker> mSpellChecker;
-
- private final TextEditorSpellChecker mEditorSpeller;
-
- /**
- * Called when the definition data is changed.
- */
- private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
- event -> {
- process( getTextEditor() );
- save( getTextDefinition() );
- };
-
- /**
- * Tracks the number of detached tab panels opened into their own windows,
- * which allows unique identification of subordinate windows by their title.
- * It is doubtful more than 128 windows, much less 256, will be created.
- */
- private byte mWindowCount;
-
- private final VariableNameInjector mVariableNameInjector;
-
- private final RBootstrapController mRBootstrapController;
-
- private final DocumentStatistics mStatistics;
-
- @SuppressWarnings( {"FieldCanBeLocal", "unused"} )
- private final TypesetterInstaller mInstallWizard;
-
- /**
- * Adds all content panels to the main user interface. This will load the
- * configuration settings from the workspace to reproduce the settings from
- * a previous session.
- */
- public MainPane( final Workspace workspace ) {
- mWorkspace = workspace;
- mSpellChecker = createSpellChecker();
- mEditorSpeller = createTextEditorSpellChecker( mSpellChecker );
- mPreview = new HtmlPreview( workspace );
- mStatistics = new DocumentStatistics( workspace );
-
- mTextEditor.addListener( ( c, o, n ) -> {
- if( o != null ) {
- removeProcessor( o );
- }
-
- if( n != null ) {
- mPreview.setBaseUri( n.getPath() );
- updateProcessors( n );
- process( n );
- }
- } );
-
- mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
- mDefinitionEditor.set( createDefinitionEditor( workspace ) );
- mVariableNameInjector = new VariableNameInjector( workspace );
- mRBootstrapController = new RBootstrapController(
- workspace, mDefinitionEditor.get()::getDefinitions
- );
-
- // If the user modifies the definitions, re-process the variables.
- mDefinitionEditor.addListener( ( c, o, n ) -> {
- final var textEditor = getTextEditor();
-
- if( textEditor.isMediaType( TEXT_R_MARKDOWN ) ) {
- mRBootstrapController.update();
- }
-
- process( textEditor );
- } );
-
- open( collect( getRecentFiles() ) );
- viewPreview();
- setDividerPositions( calculateDividerPositions() );
-
- // Once the main scene's window regains focus, update the active definition
- // editor to the currently selected tab.
- runLater( () -> getWindow().setOnCloseRequest( event -> {
- // Order matters: Open file names must be persisted before closing all.
- mWorkspace.save();
-
- if( closeAll() ) {
- exit();
- terminate( 0 );
- }
-
- event.consume();
- } ) );
-
- register( this );
- initAutosave( workspace );
-
- restoreSession();
- runLater( this::restoreFocus );
-
- mInstallWizard = new TypesetterInstaller( workspace );
- }
-
- /**
- * Called when spellchecking can be run. This will reload the dictionary
- * into memory once, and then re-use it for all the existing text editors.
- *
- * @param event The event to process, having a populated word-frequency map.
- */
- @Subscribe
- public void handle( final LexiconLoadedEvent event ) {
- final var lexicon = event.getLexicon();
-
- try {
- final var checker = SymSpellSpeller.forLexicon( lexicon );
- mSpellChecker.set( checker );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- @Subscribe
- public void handle( final TextEditorFocusEvent event ) {
- mTextEditor.set( event.get() );
- }
-
- @Subscribe
- public void handle( final TextDefinitionFocusEvent event ) {
- mDefinitionEditor.set( event.get() );
- }
-
- /**
- * Typically called when a file name is clicked in the preview panel.
- *
- * @param event The event to process, must contain a valid file reference.
- */
- @Subscribe
- public void handle( final FileOpenEvent event ) {
- final File eventFile;
- final var eventUri = event.getUri();
-
- if( eventUri.isAbsolute() ) {
- eventFile = new File( eventUri.getPath() );
- }
- else {
- final var activeFile = getTextEditor().getFile();
- final var parent = activeFile.getParentFile();
-
- if( parent == null ) {
- clue( new FileNotFoundException( eventUri.getPath() ) );
- return;
- }
- else {
- final var parentPath = parent.getAbsolutePath();
- eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) );
- }
- }
-
- final var mediaType = MediaTypeExtension.fromFile( eventFile );
-
- runLater( () -> {
- // Open text files locally.
- if( mediaType.isType( TEXT ) ) {
- open( eventFile );
- }
- else {
- try {
- // Delegate opening all other file types to the operating system.
- getDesktop().open( eventFile );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
- } );
- }
-
- @Subscribe
- public void handle( final CaretNavigationEvent event ) {
- runLater( () -> {
- final var textArea = getTextEditor();
- textArea.moveTo( event.getOffset() );
- textArea.requestFocus();
- } );
- }
-
- @Subscribe
- public void handle( final InsertDefinitionEvent<String> event ) {
- final var leaf = event.getLeaf();
- final var editor = mTextEditor.get();
-
- mVariableNameInjector.insert( editor, leaf );
- }
-
- private void initAutosave( final Workspace workspace ) {
- final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE );
-
- rate.addListener(
- ( c, o, n ) -> {
- final var taskRef = mSaveTask.get();
-
- // Prevent multiple autosaves from running.
- if( taskRef != null ) {
- taskRef.cancel( false );
- }
-
- initAutosave( rate );
- }
- );
-
- // Start the save listener (avoids duplicating some code).
- initAutosave( rate );
- }
-
- private void initAutosave( final IntegerProperty rate ) {
- mSaveTask.set(
- mSaver.scheduleAtFixedRate(
- () -> {
- if( getTextEditor().isModified() ) {
- // Ensure the modified indicator is cleared by running on EDT.
- runLater( this::save );
- }
- }, 0, rate.intValue(), SECONDS
- )
- );
- }
-
- /**
- * TODO: Load divider positions from exported settings, see
- * {@link #collect(SetProperty)} comment.
- */
- private double[] calculateDividerPositions() {
- final var ratio = 100f / getItems().size() / 100;
- final var positions = getDividerPositions();
-
- for( int i = 0; i < positions.length; i++ ) {
- positions[ i ] = ratio * i;
- }
-
- return positions;
- }
-
- /**
- * Opens all the files into the application, provided the paths are unique.
- * This may only be called for any type of files that a user can edit
- * (i.e., update and persist), such as definitions and text files.
- *
- * @param files The list of files to open.
- */
- public void open( final List<File> files ) {
- files.forEach( this::open );
- }
-
- /**
- * This opens the given file. Since the preview pane is not a file that
- * can be opened, it is safe to add a listener to the detachable pane.
- * This will exit early if the given file is not a regular file (i.e., a
- * directory).
- *
- * @param inputFile The file to open.
- */
- private void open( final File inputFile ) {
- // Prevent opening directories (a non-existent "untitled.md" is fine).
- if( !inputFile.isFile() && inputFile.exists() ) {
- return;
- }
-
- final var mediaType = fromFilename( inputFile );
-
- // Only allow opening text files.
- if( !mediaType.isType( TEXT ) ) {
- return;
- }
-
- final var tab = createTab( inputFile );
- final var node = tab.getContent();
- final var tabPane = obtainTabPane( mediaType );
-
- tab.setTooltip( createTooltip( inputFile ) );
- tabPane.setFocusTraversable( false );
- tabPane.setTabClosingPolicy( ALL_TABS );
- tabPane.getTabs().add( tab );
-
- // Attach the tab scene factory for new tab panes.
- if( !getItems().contains( tabPane ) ) {
- addTabPane(
- node instanceof TextDefinition ? 0 : getItems().size(), tabPane
- );
- }
-
- if( inputFile.isFile() ) {
- getRecentFiles().add( inputFile.getAbsolutePath() );
- }
- }
-
- /**
- * Gives focus to the most recently edited document and attempts to move
- * the caret to the most recently known offset into said document.
- */
- private void restoreSession() {
- final var workspace = getWorkspace();
- final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT );
- final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET );
-
- for( final var pane : mTabPanes ) {
- for( final var tab : pane.getTabs() ) {
- final var tooltip = tab.getTooltip();
-
- if( tooltip != null ) {
- final var tabName = tooltip.getText();
- final var fileName = file.get().toString();
-
- if( tabName.equalsIgnoreCase( fileName ) ) {
- final var node = tab.getContent();
-
- pane.getSelectionModel().select( tab );
- node.requestFocus();
-
- if( node instanceof TextEditor editor ) {
- runLater( () -> editor.moveTo( offset.getValue() ) );
- }
-
- break;
- }
- }
- }
- }
- }
-
- /**
- * Sets the focus to the middle pane, which contains the text editor tabs.
- */
- private void restoreFocus() {
- // Work around a bug where focusing directly on the middle pane results
- // in the R engine not loading variables properly.
- mTabPanes.get( 0 ).requestFocus();
-
- // This is the only line that should be required.
- mTabPanes.get( 1 ).requestFocus();
- }
-
- /**
- * Opens a new text editor document using the default document file name.
- */
- public void newTextEditor() {
- open( DOCUMENT_DEFAULT );
+import com.keenwrite.constants.Constants;
+import com.keenwrite.editors.TextDefinition;
+import com.keenwrite.editors.TextEditor;
+import com.keenwrite.editors.TextResource;
+import com.keenwrite.editors.common.ScrollEventHandler;
+import com.keenwrite.editors.common.VariableNameInjector;
+import com.keenwrite.editors.definition.DefinitionEditor;
+import com.keenwrite.editors.definition.TreeTransformer;
+import com.keenwrite.editors.definition.yaml.YamlTreeTransformer;
+import com.keenwrite.editors.markdown.MarkdownEditor;
+import com.keenwrite.events.*;
+import com.keenwrite.events.spelling.LexiconLoadedEvent;
+import com.keenwrite.io.MediaType;
+import com.keenwrite.io.MediaTypeExtension;
+import com.keenwrite.preferences.Workspace;
+import com.keenwrite.preview.HtmlPreview;
+import com.keenwrite.processors.HtmlPreviewProcessor;
+import com.keenwrite.processors.Processor;
+import com.keenwrite.processors.ProcessorContext;
+import com.keenwrite.processors.ProcessorFactory;
+import com.keenwrite.processors.r.Engine;
+import com.keenwrite.processors.r.RBootstrapController;
+import com.keenwrite.service.events.Notifier;
+import com.keenwrite.spelling.api.SpellChecker;
+import com.keenwrite.spelling.impl.PermissiveSpeller;
+import com.keenwrite.spelling.impl.SymSpellSpeller;
+import com.keenwrite.typesetting.installer.TypesetterInstaller;
+import com.keenwrite.ui.explorer.FilePickerFactory;
+import com.keenwrite.ui.heuristics.DocumentStatistics;
+import com.keenwrite.ui.outline.DocumentOutline;
+import com.keenwrite.ui.spelling.TextEditorSpellChecker;
+import com.keenwrite.util.GenericBuilder;
+import com.panemu.tiwulfx.control.dock.DetachableTab;
+import com.panemu.tiwulfx.control.dock.DetachableTabPane;
+import javafx.beans.property.*;
+import javafx.collections.ListChangeListener;
+import javafx.concurrent.Task;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.control.Tooltip;
+import javafx.scene.control.TreeItem.TreeModificationEvent;
+import javafx.scene.input.KeyEvent;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+import org.greenrobot.eventbus.Subscribe;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static com.keenwrite.ExportFormat.NONE;
+import static com.keenwrite.Launcher.terminate;
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.constants.Constants.*;
+import static com.keenwrite.events.Bus.register;
+import static com.keenwrite.events.StatusEvent.clue;
+import static com.keenwrite.io.MediaType.*;
+import static com.keenwrite.io.MediaType.TypeName.TEXT;
+import static com.keenwrite.io.SysFile.toFile;
+import static com.keenwrite.preferences.AppKeys.*;
+import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
+import static com.keenwrite.processors.ProcessorContext.Mutator;
+import static com.keenwrite.processors.ProcessorContext.builder;
+import static com.keenwrite.processors.ProcessorFactory.createProcessors;
+import static java.awt.Desktop.getDesktop;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.groupingBy;
+import static javafx.application.Platform.exit;
+import static javafx.application.Platform.runLater;
+import static javafx.scene.control.ButtonType.NO;
+import static javafx.scene.control.ButtonType.YES;
+import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyCode.SPACE;
+import static javafx.scene.input.KeyCombination.ALT_DOWN;
+import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
+import static javafx.util.Duration.millis;
+import static javax.swing.SwingUtilities.invokeLater;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
+
+/**
+ * Responsible for wiring together the main application components for a
+ * particular {@link Workspace} (project). These include the definition views,
+ * text editors, and preview pane along with any corresponding controllers.
+ */
+public final class MainPane extends SplitPane {
+
+ private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
+ private static final Notifier sNotifier = Services.load( Notifier.class );
+
+ /**
+ * Used when opening files to determine how each file should be binned and
+ * therefore what tab pane to be opened within.
+ */
+ private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
+ TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED
+ );
+
+ private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 );
+ private final AtomicReference<ScheduledFuture<?>> mSaveTask =
+ new AtomicReference<>();
+
+ /**
+ * Prevents re-instantiation of processing classes.
+ */
+ private final Map<TextResource, Processor<String>> mProcessors =
+ new HashMap<>();
+
+ private final Workspace mWorkspace;
+
+ /**
+ * Groups similar file type tabs together.
+ */
+ private final List<TabPane> mTabPanes = new ArrayList<>();
+
+ /**
+ * Renders the actively selected plain text editor tab.
+ */
+ private final HtmlPreview mPreview;
+
+ /**
+ * Provides an interactive document outline.
+ */
+ private final DocumentOutline mOutline = new DocumentOutline();
+
+ /**
+ * Changing the active editor fires the value changed event. This allows
+ * refreshes to happen when external definitions are modified and need to
+ * trigger the processing chain.
+ */
+ private final ObjectProperty<TextEditor> mTextEditor =
+ new SimpleObjectProperty<>();
+
+ /**
+ * Changing the active definition editor fires the value changed event. This
+ * allows refreshes to happen when external definitions are modified and need
+ * to trigger the processing chain.
+ */
+ private final ObjectProperty<TextDefinition> mDefinitionEditor =
+ new SimpleObjectProperty<>();
+
+ private final ObjectProperty<SpellChecker> mSpellChecker;
+
+ private final TextEditorSpellChecker mEditorSpeller;
+
+ /**
+ * Called when the definition data is changed.
+ */
+ private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
+ _ -> {
+ process( getTextEditor() );
+ save( getTextDefinition() );
+ };
+
+ /**
+ * Tracks the number of detached tab panels opened into their own windows,
+ * which allows unique identification of subordinate windows by their title.
+ * It is doubtful more than 128 windows, much less 256, will be created.
+ */
+ private byte mWindowCount;
+
+ private final VariableNameInjector mVariableNameInjector;
+
+ private final RBootstrapController mRBootstrapController;
+
+ private final DocumentStatistics mStatistics;
+
+ @SuppressWarnings( { "FieldCanBeLocal", "unused" } )
+ private final TypesetterInstaller mInstallWizard;
+
+ /**
+ * Adds all content panels to the main user interface. This will load the
+ * configuration settings from the workspace to reproduce the settings from
+ * a previous session.
+ */
+ public MainPane( final Workspace workspace ) {
+ mWorkspace = workspace;
+ mSpellChecker = createSpellChecker();
+ mEditorSpeller = createTextEditorSpellChecker( mSpellChecker );
+ mPreview = new HtmlPreview( workspace );
+ mStatistics = new DocumentStatistics( workspace );
+
+ mTextEditor.addListener( ( c, o, n ) -> {
+ if( o != null ) {
+ removeProcessor( o );
+ }
+
+ if( n != null ) {
+ mPreview.setBaseUri( n.getPath() );
+ updateProcessors( n );
+ process( n );
+ }
+ } );
+
+ mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
+ mDefinitionEditor.set( createDefinitionEditor( workspace ) );
+ mVariableNameInjector = new VariableNameInjector( workspace );
+ mRBootstrapController = new RBootstrapController(
+ workspace, mDefinitionEditor.get()::getDefinitions
+ );
+
+ // If the user modifies the definitions, re-process the variables.
+ mDefinitionEditor.addListener( ( c, o, n ) -> {
+ final var textEditor = getTextEditor();
+
+ if( textEditor.isMediaType( TEXT_R_MARKDOWN ) ) {
+ mRBootstrapController.update();
+ }
+
+ process( textEditor );
+ } );
+
+ open( collect( getRecentFiles() ) );
+ viewPreview();
+ setDividerPositions( calculateDividerPositions() );
+
+ // Once the main scene's window regains focus, update the active definition
+ // editor to the currently selected tab.
+ runLater( () -> getWindow().setOnCloseRequest( event -> {
+ // Order matters: Open file names must be persisted before closing all.
+ mWorkspace.save();
+
+ if( closeAll() ) {
+ exit();
+ terminate( 0 );
+ }
+
+ event.consume();
+ } ) );
+
+ register( this );
+ initAutosave( workspace );
+
+ restoreSession();
+ runLater( this::restoreFocus );
+
+ mInstallWizard = new TypesetterInstaller( workspace );
+ }
+
+ /**
+ * Called when spellchecking can be run. This will reload the dictionary
+ * into memory once, and then re-use it for all the existing text editors.
+ *
+ * @param event The event to process, having a populated word-frequency map.
+ */
+ @Subscribe
+ public void handle( final LexiconLoadedEvent event ) {
+ final var lexicon = event.getLexicon();
+
+ try {
+ final var checker = SymSpellSpeller.forLexicon( lexicon );
+ mSpellChecker.set( checker );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ @Subscribe
+ public void handle( final TextEditorFocusEvent event ) {
+ mTextEditor.set( event.get() );
+ }
+
+ @Subscribe
+ public void handle( final TextDefinitionFocusEvent event ) {
+ mDefinitionEditor.set( event.get() );
+ }
+
+ /**
+ * Typically called when a file name is clicked in the preview panel.
+ *
+ * @param event The event to process, must contain a valid file reference.
+ */
+ @Subscribe
+ public void handle( final FileOpenEvent event ) {
+ final File eventFile;
+ final var eventUri = event.getUri();
+
+ if( eventUri.isAbsolute() ) {
+ eventFile = new File( eventUri.getPath() );
+ }
+ else {
+ final var activeFile = getTextEditor().getFile();
+ final var parent = activeFile.getParentFile();
+
+ if( parent == null ) {
+ clue( new FileNotFoundException( eventUri.getPath() ) );
+ return;
+ }
+ else {
+ final var parentPath = parent.getAbsolutePath();
+ eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) );
+ }
+ }
+
+ final var mediaType = MediaTypeExtension.fromFile( eventFile );
+
+ runLater( () -> {
+ // Open text files locally.
+ if( mediaType.isType( TEXT ) ) {
+ open( eventFile );
+ }
+ else {
+ try {
+ // Delegate opening all other file types to the operating system.
+ getDesktop().open( eventFile );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+ } );
+ }
+
+ @Subscribe
+ public void handle( final CaretNavigationEvent event ) {
+ runLater( () -> {
+ final var textArea = getTextEditor();
+ textArea.moveTo( event.getOffset() );
+ textArea.requestFocus();
+ } );
+ }
+
+ @Subscribe
+ public void handle( final InsertDefinitionEvent<String> event ) {
+ final var leaf = event.getLeaf();
+ final var editor = mTextEditor.get();
+
+ mVariableNameInjector.insert( editor, leaf );
+ }
+
+ private void initAutosave( final Workspace workspace ) {
+ final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE );
+
+ rate.addListener(
+ ( c, o, n ) -> {
+ final var taskRef = mSaveTask.get();
+
+ // Prevent multiple auto-saves from running.
+ if( taskRef != null ) {
+ taskRef.cancel( false );
+ }
+
+ initAutosave( rate );
+ }
+ );
+
+ // Start the save listener (avoids duplicating some code).
+ initAutosave( rate );
+ }
+
+ private void initAutosave( final IntegerProperty rate ) {
+ mSaveTask.set(
+ mSaver.scheduleAtFixedRate(
+ () -> {
+ if( getTextEditor().isModified() ) {
+ // Ensure the modified indicator is cleared by running on EDT.
+ runLater( this::save );
+ }
+ }, 0, rate.intValue(), SECONDS
+ )
+ );
+ }
+
+ /**
+ * TODO: Load divider positions from exported settings, see
+ * {@link #collect(SetProperty)} comment.
+ */
+ private double[] calculateDividerPositions() {
+ final var ratio = 100f / getItems().size() / 100;
+ final var positions = getDividerPositions();
+
+ for( int i = 0; i < positions.length; i++ ) {
+ positions[ i ] = ratio * i;
+ }
+
+ return positions;
+ }
+
+ /**
+ * Opens all the files into the application, provided the paths are unique.
+ * This may only be called for any type of files that a user can edit
+ * (i.e., update and persist), such as definitions and text files.
+ *
+ * @param files The list of files to open.
+ */
+ public void open( final List<File> files ) {
+ files.forEach( this::open );
+ }
+
+ /**
+ * This opens the given file. Since the preview pane is not a file that
+ * can be opened, it is safe to add a listener to the detachable pane.
+ * This will exit early if the given file is not a regular file (i.e., a
+ * directory).
+ *
+ * @param inputFile The file to open.
+ */
+ private void open( final File inputFile ) {
+ // Prevent opening directories (a non-existent "untitled.md" is fine).
+ if( !inputFile.isFile() && inputFile.exists() ) {
+ return;
+ }
+
+ final var mediaType = fromFilename( inputFile );
+
+ // Only allow opening text files.
+ if( !mediaType.isType( TEXT ) ) {
+ return;
+ }
+
+ final var tab = createTab( inputFile );
+ final var node = tab.getContent();
+ final var tabPane = obtainTabPane( mediaType );
+
+ tab.setTooltip( createTooltip( inputFile ) );
+ tabPane.setFocusTraversable( false );
+ tabPane.setTabClosingPolicy( ALL_TABS );
+ tabPane.getTabs().add( tab );
+
+ // Attach the tab scene factory for new tab panes.
+ if( !getItems().contains( tabPane ) ) {
+ addTabPane(
+ node instanceof TextDefinition ? 0 : getItems().size(), tabPane
+ );
+ }
+
+ if( inputFile.isFile() ) {
+ getRecentFiles().add( inputFile.getAbsolutePath() );
+ }
+ }
+
+ /**
+ * Gives focus to the most recently edited document and attempts to move
+ * the caret to the most recently known offset into said document.
+ */
+ private void restoreSession() {
+ final var workspace = getWorkspace();
+ final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT );
+ final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET );
+
+ for( final var pane : mTabPanes ) {
+ for( final var tab : pane.getTabs() ) {
+ final var tooltip = tab.getTooltip();
+
+ if( tooltip != null ) {
+ final var tabName = tooltip.getText();
+ final var fileName = file.get().toString();
+
+ if( tabName.equalsIgnoreCase( fileName ) ) {
+ final var node = tab.getContent();
+
+ pane.getSelectionModel().select( tab );
+ node.requestFocus();
+
+ if( node instanceof TextEditor editor ) {
+ runLater( () -> editor.moveTo( offset.getValue() ) );
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the focus to the middle pane, which contains the text editor tabs.
+ */
+ private void restoreFocus() {
+ // Work around a bug where focusing directly on the middle pane results
+ // in the R engine not loading variables properly.
+ mTabPanes.get( 0 ).requestFocus();
+
+ // This is the only line that should be required.
+ mTabPanes.get( 1 ).requestFocus();
+ }
+
+ /**
+ * Opens a new text editor document using a document file name that doesn't
+ * clash with an existing document.
+ */
+ public void newTextEditor() {
+ final String key = "file.default.document.";
+ final String prefix = Constants.get( STR."\{key}prefix" );
+ final String suffix = Constants.get( STR."\{key}suffix" );
+
+ File file = new File( STR."\{prefix}.\{suffix}" );
+ int i = 0;
+
+ while( file.exists() && i++ < 100 ) {
+ file = new File( STR."\{prefix}-\{i}.\{suffix}" );
+ }
+
+ open( file );
}
src/main/java/com/keenwrite/constants/Constants.java
}
- static String get( final String key ) {
+ public static String get( final String key ) {
return sSettings.getSetting( key, "" );
}
*/
private static File getFile( final String suffix ) {
- return new File( get( "file.default." + suffix ) );
+ return new File( get( STR."file.default.\{suffix}" ) );
}
src/main/java/com/keenwrite/ui/actions/Action.java
*/
public Builder setId( final String id ) {
- final var prefix = ACTION_PREFIX + id + ".";
- final var text = prefix + "text";
- final var icon = prefix + "icon";
- final var accelerator = prefix + "accelerator";
+ final var prefix = STR."\{ACTION_PREFIX}\{id}.";
+ final var text = STR."\{prefix}text";
+ final var icon = STR."\{prefix}icon";
+ final var accelerator = STR."\{prefix}accelerator";
final var builder = setText( text ).setIcon( icon );
if( mAccelerator != null ) {
- tooltip += " (" + mAccelerator.getDisplayText() + ')';
+ tooltip += STR." (\{mAccelerator.getDisplayText()}\{')'}";
}
src/main/java/com/keenwrite/ui/actions/ApplicationBars.java
return createMenu(
get( "Main.menu.file" ),
- addAction( "file.new", e -> actions.file_new() ),
- addAction( "file.open", e -> actions.file_open() ),
+ addAction( "file.new", _ -> actions.file_new() ),
+ addAction( "file.open", _ -> actions.file_open() ),
SEPARATOR,
- addAction( "file.close", e -> actions.file_close() ),
- addAction( "file.close_all", e -> actions.file_close_all() ),
+ addAction( "file.close", _ -> actions.file_close() ),
+ addAction( "file.close_all", _ -> actions.file_close_all() ),
SEPARATOR,
- addAction( "file.save", e -> actions.file_save() ),
- addAction( "file.save_as", e -> actions.file_save_as() ),
- addAction( "file.save_all", e -> actions.file_save_all() ),
+ addAction( "file.save", _ -> actions.file_save() ),
+ addAction( "file.save_as", _ -> actions.file_save_as() ),
+ addAction( "file.save_all", _ -> actions.file_save_all() ),
SEPARATOR,
- addAction( "file.export", e -> { } )
+ addAction( "file.export", _ -> { } )
.addSubActions(
- addAction( "file.export.pdf", e -> actions.file_export_pdf() ),
- addAction( "file.export.pdf.dir", e -> actions.file_export_pdf_dir() ),
- addAction( "file.export.pdf.repeat", e -> actions.file_export_repeat() ),
- addAction( "file.export.html.dir", e -> actions.file_export_html_dir() ),
- addAction( "file.export.html_svg", e -> actions.file_export_html_svg() ),
- addAction( "file.export.html_tex", e -> actions.file_export_html_tex() ),
- addAction( "file.export.xhtml_tex", e -> actions.file_export_xhtml_tex() )
+ addAction( "file.export.pdf", _ -> actions.file_export_pdf() ),
+ addAction( "file.export.pdf.dir", _ -> actions.file_export_pdf_dir() ),
+ addAction( "file.export.pdf.repeat", _ -> actions.file_export_repeat() ),
+ addAction( "file.export.html.dir", _ -> actions.file_export_html_dir() ),
+ addAction( "file.export.html_svg", _ -> actions.file_export_html_svg() ),
+ addAction( "file.export.html_tex", _ -> actions.file_export_html_tex() ),
+ addAction( "file.export.xhtml_tex", _ -> actions.file_export_xhtml_tex() )
),
SEPARATOR,
- addAction( "file.exit", e -> actions.file_exit() )
+ addAction( "file.exit", _ -> actions.file_exit() )
);
// @formatter:on
get( "Main.menu.edit" ),
SEPARATOR,
- addAction( "edit.undo", e -> actions.edit_undo() ),
- addAction( "edit.redo", e -> actions.edit_redo() ),
+ addAction( "edit.undo", _ -> actions.edit_undo() ),
+ addAction( "edit.redo", _ -> actions.edit_redo() ),
SEPARATOR,
- addAction( "edit.cut", e -> actions.edit_cut() ),
- addAction( "edit.copy", e -> actions.edit_copy() ),
- addAction( "edit.paste", e -> actions.edit_paste() ),
- addAction( "edit.select_all", e -> actions.edit_select_all() ),
+ addAction( "edit.cut", _ -> actions.edit_cut() ),
+ addAction( "edit.copy", _ -> actions.edit_copy() ),
+ addAction( "edit.paste", _ -> actions.edit_paste() ),
+ addAction( "edit.select_all", _ -> actions.edit_select_all() ),
SEPARATOR,
- addAction( "edit.find", e -> actions.edit_find() ),
- addAction( "edit.find_next", e -> actions.edit_find_next() ),
- addAction( "edit.find_prev", e -> actions.edit_find_prev() ),
+ addAction( "edit.find", _ -> actions.edit_find() ),
+ addAction( "edit.find_next", _ -> actions.edit_find_next() ),
+ addAction( "edit.find_prev", _ -> actions.edit_find_prev() ),
SEPARATOR,
- addAction( "edit.preferences", e -> actions.edit_preferences() )
+ addAction( "edit.preferences", _ -> actions.edit_preferences() )
);
}
@NotNull
private static Menu createMenuFormat( final GuiCommands actions ) {
return createMenu(
get( "Main.menu.format" ),
- addAction( "format.bold", e -> actions.format_bold() ),
- addAction( "format.italic", e -> actions.format_italic() ),
- addAction( "format.monospace", e -> actions.format_monospace() ),
- addAction( "format.superscript", e -> actions.format_superscript() ),
- addAction( "format.subscript", e -> actions.format_subscript() ),
- addAction( "format.strikethrough", e -> actions.format_strikethrough() )
+ addAction( "format.bold", _ -> actions.format_bold() ),
+ addAction( "format.italic", _ -> actions.format_italic() ),
+ addAction( "format.monospace", _ -> actions.format_monospace() ),
+ addAction( "format.superscript", _ -> actions.format_superscript() ),
+ addAction( "format.subscript", _ -> actions.format_subscript() ),
+ addAction( "format.strikethrough", _ -> actions.format_strikethrough() )
);
}
return createMenu(
get( "Main.menu.insert" ),
- addAction( "insert.blockquote", e -> actions.insert_blockquote() ),
- addAction( "insert.code", e -> actions.insert_code() ),
- addAction( "insert.fenced_code_block", e -> actions.insert_fenced_code_block() ),
+ addAction( "insert.blockquote", _ -> actions.insert_blockquote() ),
+ addAction( "insert.code", _ -> actions.insert_code() ),
+ addAction( "insert.fenced_code_block", _ -> actions.insert_fenced_code_block() ),
SEPARATOR,
- addAction( "insert.link", e -> actions.insert_link() ),
- addAction( "insert.image", e -> actions.insert_image() ),
+ addAction( "insert.link", _ -> actions.insert_link() ),
+ addAction( "insert.image", _ -> actions.insert_image() ),
SEPARATOR,
- addAction( "insert.heading_1", e -> actions.insert_heading_1() ),
- addAction( "insert.heading_2", e -> actions.insert_heading_2() ),
- addAction( "insert.heading_3", e -> actions.insert_heading_3() ),
+ addAction( "insert.heading_1", _ -> actions.insert_heading_1() ),
+ addAction( "insert.heading_2", _ -> actions.insert_heading_2() ),
+ addAction( "insert.heading_3", _ -> actions.insert_heading_3() ),
SEPARATOR,
- addAction( "insert.unordered_list", e -> actions.insert_unordered_list() ),
- addAction( "insert.ordered_list", e -> actions.insert_ordered_list() ),
- addAction( "insert.horizontal_rule", e -> actions.insert_horizontal_rule() )
+ addAction( "insert.unordered_list", _ -> actions.insert_unordered_list() ),
+ addAction( "insert.ordered_list", _ -> actions.insert_ordered_list() ),
+ addAction( "insert.horizontal_rule", _ -> actions.insert_horizontal_rule() )
);
// @formatter:on
}
@NotNull
private static Menu createMenuVariable(
final GuiCommands actions, final SeparatorAction SEPARATOR ) {
return createMenu(
get( "Main.menu.definition" ),
- addAction( "definition.insert", e -> actions.definition_autoinsert() ),
+ addAction( "definition.insert", _ -> actions.definition_autoinsert() ),
SEPARATOR,
- addAction( "definition.create", e -> actions.definition_create() ),
- addAction( "definition.rename", e -> actions.definition_rename() ),
- addAction( "definition.delete", e -> actions.definition_delete() )
+ addAction( "definition.create", _ -> actions.definition_create() ),
+ addAction( "definition.rename", _ -> actions.definition_rename() ),
+ addAction( "definition.delete", _ -> actions.definition_delete() )
);
}
@NotNull
private static Menu createMenuView(
final GuiCommands actions, final SeparatorAction SEPARATOR ) {
return createMenu(
get( "Main.menu.view" ),
- addAction( "view.refresh", e -> actions.view_refresh() ),
+ addAction( "view.refresh", _ -> actions.view_refresh() ),
SEPARATOR,
- addAction( "view.preview", e -> actions.view_preview() ),
- addAction( "view.outline", e -> actions.view_outline() ),
- addAction( "view.statistics", e -> actions.view_statistics() ),
- addAction( "view.files", e -> actions.view_files() ),
+ addAction( "view.preview", _ -> actions.view_preview() ),
+ addAction( "view.outline", _ -> actions.view_outline() ),
+ addAction( "view.statistics", _ -> actions.view_statistics() ),
+ addAction( "view.files", _ -> actions.view_files() ),
SEPARATOR,
- addAction( "view.menubar", e -> actions.view_menubar() ),
- addAction( "view.toolbar", e -> actions.view_toolbar() ),
- addAction( "view.statusbar", e -> actions.view_statusbar() ),
+ addAction( "view.menubar", _ -> actions.view_menubar() ),
+ addAction( "view.toolbar", _ -> actions.view_toolbar() ),
+ addAction( "view.statusbar", _ -> actions.view_statusbar() ),
SEPARATOR,
- addAction( "view.log", e -> actions.view_log() )
+ addAction( "view.log", _ -> actions.view_log() )
);
}
@NotNull
private static Menu createMenuHelp( final GuiCommands actions ) {
return createMenu(
get( "Main.menu.help" ),
- addAction( "help.about", e -> actions.help_about() )
+ addAction( "help.about", _ -> actions.help_about() )
);
}
src/main/resources/com/keenwrite/settings.properties
# discerned so that the correct type of variable
# reference can be inserted.
-file.default.document=untitled.md
+file.default.document.prefix=untitled
+file.default.document.suffix=md
+file.default.document=${file.default.document.prefix}.${file.default.document.suffix}
file.default.definition=variables.yaml

Fixes new file bug, clarifies cross-reference names

Author DaveJarvis <email>
Date 2023-12-16 10:01:46 GMT-0800
Commit 8196ce25e28590e7670aff47b6b129c666cd78b9
Parent b5ea983
Delta 580 lines added, 565 lines removed, 15-line increase