Dave Jarvis' Repositories

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

Fix startup race-condition bug, library upgrades, add comments

Author DaveJarvis <email>
Date 2023-06-24 19:30:49 GMT-0700
Commit 08d8f32a80791c15dd6d0423eecd0b3cb23ca8cd
Parent dc1771c
README.md
1. Download the *Full version* of the Java Runtime Environment, [JRE 20](https://bell-sw.com/pages/downloads).
+ * Note that both Java 20+ and JavaFX are required. The *Full version* of
+ BellSoft's JRE satisifies these requirements.
1. Install the JRE (include JRE's `bin` directory in the `PATH` environment variable).
1. Open a new terminal.
build.gradle
def v_junit = '5.9.3'
def v_flexmark = '0.64.6'
- def v_jackson = '2.15.1'
- def v_echosvg = '0.3'
- def v_picocli = '4.7.3'
+ def v_jackson = '2.15.2'
+ def v_echosvg = '0.3.1'
+ def v_picocli = '4.7.4'
// JavaFX
// R
implementation 'org.apache.commons:commons-compress:1.23.0'
- implementation 'org.codehaus.plexus:plexus-utils:3.5.1'
+ implementation 'org.codehaus.plexus:plexus-utils:4.0.0'
implementation 'org.renjin:renjin-script-engine:3.5-beta76'
implementation 'org.renjin.cran:rjson:0.2.15-renjin-21'
src/main/java/com/keenwrite/MainPane.java
import com.panemu.tiwulfx.control.dock.DetachableTab;
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
-import javafx.application.Platform;
-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.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.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 =
- createActiveTextEditor();
-
- /**
- * 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;
-
- 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.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
- mDefinitionEditor = createActiveDefinitionEditor( mTextEditor );
- mVariableNameInjector = new VariableNameInjector( mWorkspace );
- mRBootstrapController = new RBootstrapController(
- mWorkspace, this::getDefinitions );
-
- 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() ) {
- Platform.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 = Path.of( parentPath, eventUri.getPath() ).toFile();
- }
- }
-
- 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 tab = createTab( inputFile );
- final var node = tab.getContent();
- final var mediaType = MediaType.valueFrom( inputFile );
- 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.getValue().toString();
-
- if( tabName.equalsIgnoreCase( fileName ) ) {
- final var node = tab.getContent();
-
- pane.getSelectionModel().select( tab );
- node.requestFocus();
-
- if( node instanceof TextEditor editor ) {
- 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 );
- }
-
- /**
- * Opens a new definition editor document using the default definition
- * file name.
- */
+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.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 =
+ createActiveTextEditor();
+
+ /**
+ * 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;
+
+ 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.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
+ mDefinitionEditor = createActiveDefinitionEditor( mTextEditor );
+ mVariableNameInjector = new VariableNameInjector( mWorkspace );
+ mRBootstrapController = new RBootstrapController(
+ mWorkspace, this::getDefinitions );
+
+ 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 = Path.of( parentPath, eventUri.getPath() ).toFile();
+ }
+ }
+
+ 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 tab = createTab( inputFile );
+ final var node = tab.getContent();
+ final var mediaType = MediaType.valueFrom( inputFile );
+ 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.getValue().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 );
+ }
+
+ /**
+ * Opens a new definition editor document using the default definition
+ * file name.
+ */
+ @SuppressWarnings( "unused" )
public void newDefinitionEditor() {
open( DEFINITION_DEFAULT );
src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
assert 0 <= offset && offset <= mTextArea.getLength();
- mTextArea.moveTo( offset );
- mTextArea.requestFollowCaret();
+ if( offset <= mTextArea.getLength() ) {
+ mTextArea.moveTo( offset );
+ mTextArea.requestFollowCaret();
+ }
}
src/main/java/com/keenwrite/preferences/Workspace.java
property.setValue( unmarshalled );
- } catch( final NoSuchElementException ignored ) {
+ } catch( final NoSuchElementException ex ) {
// When no configuration (item), use the default value.
+ clue( ex );
}
} );
* @return The value associated with the given {@link Key}.
*/
+ @SuppressWarnings( "unused" )
public int getInteger( final Key key ) {
assert key != null;
Delta 451 lines added, 444 lines removed, 7-line increase