| | private final ChangeListener<Integer> mCaretPositionListener = |
| | ( observable, oldPosition, newPosition ) -> { |
| | - updateCaretStatus( getActiveFileEditorTab() ); |
| | - scrollToCaret(); |
| | - }; |
| | - |
| | - private DefinitionSource mDefinitionSource = createDefaultDefinitionSource(); |
| | - private final DefinitionPane mDefinitionPane = createDefinitionPane(); |
| | - private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane(); |
| | - private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane( |
| | - mCaretPositionListener ); |
| | - |
| | - /** |
| | - * Listens on the definition pane for double-click events. |
| | - */ |
| | - private final DefinitionNameInjector mDefinitionNameInjector |
| | - = new DefinitionNameInjector( mDefinitionPane ); |
| | - |
| | - public MainWindow() { |
| | - mStatusBar = createStatusBar(); |
| | - mLineNumberText = createLineNumberText(); |
| | - mFindTextField = createFindTextField(); |
| | - mScene = createScene(); |
| | - mSpellChecker = createSpellChecker(); |
| | - |
| | - // Add the close request listener before the window is shown. |
| | - initLayout(); |
| | - StatusBarNotifier.setStatusBar( mStatusBar ); |
| | - } |
| | - |
| | - /** |
| | - * Called after the stage is shown. |
| | - */ |
| | - public void init() { |
| | - initFindInput(); |
| | - initSnitch(); |
| | - initDefinitionListener(); |
| | - initTabAddedListener(); |
| | - initTabChangedListener(); |
| | - initPreferences(); |
| | - initVariableNameInjector(); |
| | - } |
| | - |
| | - private void initLayout() { |
| | - final var scene = getScene(); |
| | - |
| | - scene.getStylesheets().add( STYLESHEET_SCENE ); |
| | - scene.windowProperty().addListener( |
| | - ( unused, oldWindow, newWindow ) -> |
| | - newWindow.setOnCloseRequest( |
| | - e -> { |
| | - if( !getFileEditorPane().closeAllEditors() ) { |
| | - e.consume(); |
| | - } |
| | - } |
| | - ) |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * Initialize the find input text field to listen on F3, ENTER, and |
| | - * ESCAPE key presses. |
| | - */ |
| | - private void initFindInput() { |
| | - final TextField input = getFindTextField(); |
| | - |
| | - input.setOnKeyPressed( ( KeyEvent event ) -> { |
| | - switch( event.getCode() ) { |
| | - case F3: |
| | - case ENTER: |
| | - editFindNext(); |
| | - break; |
| | - case F: |
| | - if( !event.isControlDown() ) { |
| | - break; |
| | - } |
| | - case ESCAPE: |
| | - getStatusBar().setGraphic( null ); |
| | - getActiveFileEditorTab().getEditorPane().requestFocus(); |
| | - break; |
| | - } |
| | - } ); |
| | - |
| | - // Remove when the input field loses focus. |
| | - input.focusedProperty().addListener( |
| | - ( focused, oldFocus, newFocus ) -> { |
| | - if( !newFocus ) { |
| | - getStatusBar().setGraphic( null ); |
| | - } |
| | - } |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * Watch for changes to external files. In particular, this awaits |
| | - * modifications to any XSL files associated with XML files being edited. |
| | - * When |
| | - * an XSL file is modified (external to the application), the snitch's ears |
| | - * perk up and the file is reloaded. This keeps the XSL transformation up to |
| | - * date with what's on the file system. |
| | - */ |
| | - private void initSnitch() { |
| | - SNITCH.addObserver( this ); |
| | - } |
| | - |
| | - /** |
| | - * Listen for {@link FileEditorTabPane} to receive open definition file |
| | - * event. |
| | - */ |
| | - private void initDefinitionListener() { |
| | - getFileEditorPane().onOpenDefinitionFileProperty().addListener( |
| | - ( final ObservableValue<? extends Path> file, |
| | - final Path oldPath, final Path newPath ) -> { |
| | - openDefinitions( newPath ); |
| | - rerender(); |
| | - } |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * Re-instantiates all processors then re-renders the active tab. This |
| | - * will refresh the resolved map, force R to re-initialize, and brute-force |
| | - * XSLT file reloads. |
| | - */ |
| | - private void rerender() { |
| | - runLater( |
| | - () -> { |
| | - resetProcessors(); |
| | - renderActiveTab(); |
| | - } |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * When tabs are added, hook the various change listeners onto the new |
| | - * tab sothat the preview pane refreshes as necessary. |
| | - */ |
| | - private void initTabAddedListener() { |
| | - final FileEditorTabPane editorPane = getFileEditorPane(); |
| | - |
| | - // Make sure the text processor kicks off when new files are opened. |
| | - final ObservableList<Tab> tabs = editorPane.getTabs(); |
| | - |
| | - // Update the preview pane on tab changes. |
| | - tabs.addListener( |
| | - ( final Change<? extends Tab> change ) -> { |
| | - while( change.next() ) { |
| | - if( change.wasAdded() ) { |
| | - // Multiple tabs can be added simultaneously. |
| | - for( final Tab newTab : change.getAddedSubList() ) { |
| | - final FileEditorTab tab = (FileEditorTab) newTab; |
| | - |
| | - initTextChangeListener( tab ); |
| | - initScrollEventListener( tab ); |
| | - initSpellCheckListener( tab ); |
| | -// initSyntaxListener( tab ); |
| | - } |
| | - } |
| | - } |
| | - } |
| | - ); |
| | - } |
| | - |
| | - private void initTextChangeListener( final FileEditorTab tab ) { |
| | - tab.addTextChangeListener( |
| | - ( __, ov, nv ) -> { |
| | - process( tab ); |
| | - } |
| | - ); |
| | - } |
| | - |
| | - private void initScrollEventListener( final FileEditorTab tab ) { |
| | - final var scrollPane = tab.getScrollPane(); |
| | - final var scrollBar = getPreviewPane().getVerticalScrollBar(); |
| | - |
| | - addShowListener( scrollPane, ( __ ) -> { |
| | - final var handler = new ScrollEventHandler( scrollPane, scrollBar ); |
| | - handler.enabledProperty().bind( tab.selectedProperty() ); |
| | - } ); |
| | - } |
| | - |
| | - /** |
| | - * Listen for changes to the any particular paragraph and perform a quick |
| | - * spell check upon it. The style classes in the editor will be changed to |
| | - * mark any spelling mistakes in the paragraph. The user may then interact |
| | - * with any misspelled word (i.e., any piece of text that is marked) to |
| | - * revise the spelling. |
| | - * |
| | - * @param tab The tab to spellcheck. |
| | - */ |
| | - private void initSpellCheckListener( final FileEditorTab tab ) { |
| | - final var editor = tab.getEditorPane().getEditor(); |
| | - |
| | - // When the editor first appears, run a full spell check. This allows |
| | - // spell checking while typing to be restricted to the active paragraph, |
| | - // which is usually substantially smaller than the whole document. |
| | - addShowListener( |
| | - editor, ( __ ) -> spellcheck( editor, editor.getText() ) |
| | - ); |
| | - |
| | - // Use the plain text changes so that notifications of style changes |
| | - // are suppressed. Checking against the identity ensures that only |
| | - // new text additions or deletions trigger proofreading. |
| | - editor.plainTextChanges() |
| | - .filter( p -> !p.isIdentity() ).subscribe( change -> { |
| | - |
| | - // Only perform a spell check on the current paragraph. The |
| | - // entire document is processed once, when opened. |
| | - final var offset = change.getPosition(); |
| | - final var position = editor.offsetToPosition( offset, Forward ); |
| | - final var paraId = position.getMajor(); |
| | - final var paragraph = editor.getParagraph( paraId ); |
| | - final var text = paragraph.getText(); |
| | - |
| | - // Ensure that styles aren't doubled-up. |
| | - editor.clearStyle( paraId ); |
| | - |
| | - spellcheck( editor, text, paraId ); |
| | - } ); |
| | - } |
| | - |
| | - /** |
| | - * Listen for new tab selection events. |
| | - */ |
| | - private void initTabChangedListener() { |
| | - final FileEditorTabPane editorPane = getFileEditorPane(); |
| | - |
| | - // Update the preview pane changing tabs. |
| | - editorPane.addTabSelectionListener( |
| | - ( __, oldTab, newTab ) -> { |
| | - if( newTab == null ) { |
| | - // Clear the preview pane when closing an editor. When the last |
| | - // tab is closed, this ensures that the preview pane is empty. |
| | - getPreviewPane().clear(); |
| | - } |
| | - else { |
| | - final var tab = (FileEditorTab) newTab; |
| | - updateVariableNameInjector( tab ); |
| | - process( tab ); |
| | - } |
| | - } |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * Reloads the preferences from the previous session. |
| | - */ |
| | - private void initPreferences() { |
| | - initDefinitionPane(); |
| | - getFileEditorPane().initPreferences(); |
| | - getUserPreferences().addSaveEventHandler( mRPreferencesListener ); |
| | - } |
| | - |
| | - private void initVariableNameInjector() { |
| | - updateVariableNameInjector( getActiveFileEditorTab() ); |
| | - } |
| | - |
| | - /** |
| | - * Calls the listener when the given node is shown for the first time. The |
| | - * visible property is not the same as the initial showing event; visibility |
| | - * can be triggered numerous times (such as going off screen). |
| | - * <p> |
| | - * This is called, for example, before the drag handler can be attached, |
| | - * because the scrollbar for the text editor pane must be visible. |
| | - * </p> |
| | - * |
| | - * @param node The node to watch for showing. |
| | - * @param consumer The consumer to invoke when the event fires. |
| | - */ |
| | - private void addShowListener( |
| | - final Node node, final Consumer<Void> consumer ) { |
| | - final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) -> |
| | - runLater( () -> { |
| | - if( newShow != null && newShow ) { |
| | - try { |
| | - consumer.accept( null ); |
| | - } catch( final Exception ex ) { |
| | - clue( ex ); |
| | - } |
| | - } |
| | - } ); |
| | - |
| | - Val.flatMap( node.sceneProperty(), Scene::windowProperty ) |
| | - .flatMap( Window::showingProperty ) |
| | - .addListener( listener ); |
| | - } |
| | - |
| | - private void scrollToCaret() { |
| | - synchronized( mMutex ) { |
| | - final var previewPane = getPreviewPane(); |
| | - |
| | - previewPane.scrollTo( CARET_ID ); |
| | - previewPane.repaintScrollPane(); |
| | - } |
| | - } |
| | - |
| | - private void updateVariableNameInjector( final FileEditorTab tab ) { |
| | - getDefinitionNameInjector().addListener( tab ); |
| | - } |
| | - |
| | - /** |
| | - * Called to update the status bar's caret position when a new tab is added |
| | - * or the active tab is switched. |
| | - * |
| | - * @param tab The active tab containing a caret position to show. |
| | - */ |
| | - private void updateCaretStatus( final FileEditorTab tab ) { |
| | - getLineNumberText().setText( tab.getCaretPosition().toString() ); |
| | - } |
| | - |
| | - /** |
| | - * Called whenever the preview pane becomes out of sync with the file editor |
| | - * tab. This can be called when the text changes, the caret paragraph |
| | - * changes, or the file tab changes. |
| | - * |
| | - * @param tab The file editor tab that has been changed in some fashion. |
| | - */ |
| | - private void process( final FileEditorTab tab ) { |
| | - if( tab != null ) { |
| | - getPreviewPane().setPath( tab.getPath() ); |
| | - |
| | - final Processor<String> processor = getProcessors().computeIfAbsent( |
| | - tab, p -> createProcessors( tab ) |
| | - ); |
| | - |
| | - try { |
| | - updateCaretStatus( tab ); |
| | - processChain( processor, tab.getEditorText() ); |
| | - scrollToCaret(); |
| | - } catch( final Exception ex ) { |
| | - clue( ex ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - private void renderActiveTab() { |
| | + processActiveTab(); |
| | + }; |
| | + |
| | + private DefinitionSource mDefinitionSource = createDefaultDefinitionSource(); |
| | + private final DefinitionPane mDefinitionPane = createDefinitionPane(); |
| | + private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane(); |
| | + private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane( |
| | + mCaretPositionListener ); |
| | + |
| | + /** |
| | + * Listens on the definition pane for double-click events. |
| | + */ |
| | + private final DefinitionNameInjector mDefinitionNameInjector |
| | + = new DefinitionNameInjector( mDefinitionPane ); |
| | + |
| | + public MainWindow() { |
| | + mStatusBar = createStatusBar(); |
| | + mLineNumberText = createLineNumberText(); |
| | + mFindTextField = createFindTextField(); |
| | + mScene = createScene(); |
| | + mSpellChecker = createSpellChecker(); |
| | + |
| | + // Add the close request listener before the window is shown. |
| | + initLayout(); |
| | + StatusBarNotifier.setStatusBar( mStatusBar ); |
| | + } |
| | + |
| | + /** |
| | + * Called after the stage is shown. |
| | + */ |
| | + public void init() { |
| | + initFindInput(); |
| | + initSnitch(); |
| | + initDefinitionListener(); |
| | + initTabAddedListener(); |
| | + initTabChangedListener(); |
| | + initPreferences(); |
| | + initVariableNameInjector(); |
| | + } |
| | + |
| | + private void initLayout() { |
| | + final var scene = getScene(); |
| | + |
| | + scene.getStylesheets().add( STYLESHEET_SCENE ); |
| | + scene.windowProperty().addListener( |
| | + ( unused, oldWindow, newWindow ) -> |
| | + newWindow.setOnCloseRequest( |
| | + e -> { |
| | + if( !getFileEditorPane().closeAllEditors() ) { |
| | + e.consume(); |
| | + } |
| | + } |
| | + ) |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * Initialize the find input text field to listen on F3, ENTER, and |
| | + * ESCAPE key presses. |
| | + */ |
| | + private void initFindInput() { |
| | + final TextField input = getFindTextField(); |
| | + |
| | + input.setOnKeyPressed( ( KeyEvent event ) -> { |
| | + switch( event.getCode() ) { |
| | + case F3: |
| | + case ENTER: |
| | + editFindNext(); |
| | + break; |
| | + case F: |
| | + if( !event.isControlDown() ) { |
| | + break; |
| | + } |
| | + case ESCAPE: |
| | + getStatusBar().setGraphic( null ); |
| | + getActiveFileEditorTab().getEditorPane().requestFocus(); |
| | + break; |
| | + } |
| | + } ); |
| | + |
| | + // Remove when the input field loses focus. |
| | + input.focusedProperty().addListener( |
| | + ( focused, oldFocus, newFocus ) -> { |
| | + if( !newFocus ) { |
| | + getStatusBar().setGraphic( null ); |
| | + } |
| | + } |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * Watch for changes to external files. In particular, this awaits |
| | + * modifications to any XSL files associated with XML files being edited. |
| | + * When |
| | + * an XSL file is modified (external to the application), the snitch's ears |
| | + * perk up and the file is reloaded. This keeps the XSL transformation up to |
| | + * date with what's on the file system. |
| | + */ |
| | + private void initSnitch() { |
| | + SNITCH.addObserver( this ); |
| | + } |
| | + |
| | + /** |
| | + * Listen for {@link FileEditorTabPane} to receive open definition file |
| | + * event. |
| | + */ |
| | + private void initDefinitionListener() { |
| | + getFileEditorPane().onOpenDefinitionFileProperty().addListener( |
| | + ( final ObservableValue<? extends Path> file, |
| | + final Path oldPath, final Path newPath ) -> { |
| | + openDefinitions( newPath ); |
| | + rerender(); |
| | + } |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * Re-instantiates all processors then re-renders the active tab. This |
| | + * will refresh the resolved map, force R to re-initialize, and brute-force |
| | + * XSLT file reloads. |
| | + */ |
| | + private void rerender() { |
| | + runLater( |
| | + () -> { |
| | + resetProcessors(); |
| | + processActiveTab(); |
| | + } |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * When tabs are added, hook the various change listeners onto the new |
| | + * tab sothat the preview pane refreshes as necessary. |
| | + */ |
| | + private void initTabAddedListener() { |
| | + final FileEditorTabPane editorPane = getFileEditorPane(); |
| | + |
| | + // Make sure the text processor kicks off when new files are opened. |
| | + final ObservableList<Tab> tabs = editorPane.getTabs(); |
| | + |
| | + // Update the preview pane on tab changes. |
| | + tabs.addListener( |
| | + ( final Change<? extends Tab> change ) -> { |
| | + while( change.next() ) { |
| | + if( change.wasAdded() ) { |
| | + // Multiple tabs can be added simultaneously. |
| | + for( final Tab newTab : change.getAddedSubList() ) { |
| | + final FileEditorTab tab = (FileEditorTab) newTab; |
| | + |
| | + initTextChangeListener( tab ); |
| | + initScrollEventListener( tab ); |
| | + initSpellCheckListener( tab ); |
| | +// initSyntaxListener( tab ); |
| | + } |
| | + } |
| | + } |
| | + } |
| | + ); |
| | + } |
| | + |
| | + private void initTextChangeListener( final FileEditorTab tab ) { |
| | + tab.addTextChangeListener( |
| | + ( __, ov, nv ) -> { |
| | + process( tab ); |
| | + } |
| | + ); |
| | + } |
| | + |
| | + private void initScrollEventListener( final FileEditorTab tab ) { |
| | + final var scrollPane = tab.getScrollPane(); |
| | + final var scrollBar = getPreviewPane().getVerticalScrollBar(); |
| | + |
| | + addShowListener( scrollPane, ( __ ) -> { |
| | + final var handler = new ScrollEventHandler( scrollPane, scrollBar ); |
| | + handler.enabledProperty().bind( tab.selectedProperty() ); |
| | + } ); |
| | + } |
| | + |
| | + /** |
| | + * Listen for changes to the any particular paragraph and perform a quick |
| | + * spell check upon it. The style classes in the editor will be changed to |
| | + * mark any spelling mistakes in the paragraph. The user may then interact |
| | + * with any misspelled word (i.e., any piece of text that is marked) to |
| | + * revise the spelling. |
| | + * |
| | + * @param tab The tab to spellcheck. |
| | + */ |
| | + private void initSpellCheckListener( final FileEditorTab tab ) { |
| | + final var editor = tab.getEditorPane().getEditor(); |
| | + |
| | + // When the editor first appears, run a full spell check. This allows |
| | + // spell checking while typing to be restricted to the active paragraph, |
| | + // which is usually substantially smaller than the whole document. |
| | + addShowListener( |
| | + editor, ( __ ) -> spellcheck( editor, editor.getText() ) |
| | + ); |
| | + |
| | + // Use the plain text changes so that notifications of style changes |
| | + // are suppressed. Checking against the identity ensures that only |
| | + // new text additions or deletions trigger proofreading. |
| | + editor.plainTextChanges() |
| | + .filter( p -> !p.isIdentity() ).subscribe( change -> { |
| | + |
| | + // Only perform a spell check on the current paragraph. The |
| | + // entire document is processed once, when opened. |
| | + final var offset = change.getPosition(); |
| | + final var position = editor.offsetToPosition( offset, Forward ); |
| | + final var paraId = position.getMajor(); |
| | + final var paragraph = editor.getParagraph( paraId ); |
| | + final var text = paragraph.getText(); |
| | + |
| | + // Ensure that styles aren't doubled-up. |
| | + editor.clearStyle( paraId ); |
| | + |
| | + spellcheck( editor, text, paraId ); |
| | + } ); |
| | + } |
| | + |
| | + /** |
| | + * Listen for new tab selection events. |
| | + */ |
| | + private void initTabChangedListener() { |
| | + final FileEditorTabPane editorPane = getFileEditorPane(); |
| | + |
| | + // Update the preview pane changing tabs. |
| | + editorPane.addTabSelectionListener( |
| | + ( __, oldTab, newTab ) -> { |
| | + if( newTab == null ) { |
| | + // Clear the preview pane when closing an editor. When the last |
| | + // tab is closed, this ensures that the preview pane is empty. |
| | + getPreviewPane().clear(); |
| | + } |
| | + else { |
| | + final var tab = (FileEditorTab) newTab; |
| | + updateVariableNameInjector( tab ); |
| | + process( tab ); |
| | + } |
| | + } |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * Reloads the preferences from the previous session. |
| | + */ |
| | + private void initPreferences() { |
| | + initDefinitionPane(); |
| | + getFileEditorPane().initPreferences(); |
| | + getUserPreferences().addSaveEventHandler( mRPreferencesListener ); |
| | + } |
| | + |
| | + private void initVariableNameInjector() { |
| | + updateVariableNameInjector( getActiveFileEditorTab() ); |
| | + } |
| | + |
| | + /** |
| | + * Calls the listener when the given node is shown for the first time. The |
| | + * visible property is not the same as the initial showing event; visibility |
| | + * can be triggered numerous times (such as going off screen). |
| | + * <p> |
| | + * This is called, for example, before the drag handler can be attached, |
| | + * because the scrollbar for the text editor pane must be visible. |
| | + * </p> |
| | + * |
| | + * @param node The node to watch for showing. |
| | + * @param consumer The consumer to invoke when the event fires. |
| | + */ |
| | + private void addShowListener( |
| | + final Node node, final Consumer<Void> consumer ) { |
| | + final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) -> |
| | + runLater( () -> { |
| | + if( newShow != null && newShow ) { |
| | + try { |
| | + consumer.accept( null ); |
| | + } catch( final Exception ex ) { |
| | + clue( ex ); |
| | + } |
| | + } |
| | + } ); |
| | + |
| | + Val.flatMap( node.sceneProperty(), Scene::windowProperty ) |
| | + .flatMap( Window::showingProperty ) |
| | + .addListener( listener ); |
| | + } |
| | + |
| | + private void scrollToCaret() { |
| | + synchronized( mMutex ) { |
| | + final var previewPane = getPreviewPane(); |
| | + |
| | + previewPane.scrollTo( CARET_ID ); |
| | + previewPane.repaintScrollPane(); |
| | + } |
| | + } |
| | + |
| | + private void updateVariableNameInjector( final FileEditorTab tab ) { |
| | + getDefinitionNameInjector().addListener( tab ); |
| | + } |
| | + |
| | + /** |
| | + * Called to update the status bar's caret position when a new tab is added |
| | + * or the active tab is switched. |
| | + * |
| | + * @param tab The active tab containing a caret position to show. |
| | + */ |
| | + private void updateCaretStatus( final FileEditorTab tab ) { |
| | + getLineNumberText().setText( tab.getCaretPosition().toString() ); |
| | + } |
| | + |
| | + /** |
| | + * Called whenever the preview pane becomes out of sync with the file editor |
| | + * tab. This can be called when the text changes, the caret paragraph |
| | + * changes, or the file tab changes. |
| | + * |
| | + * @param tab The file editor tab that has been changed in some fashion. |
| | + */ |
| | + private void process( final FileEditorTab tab ) { |
| | + if( tab != null ) { |
| | + getPreviewPane().setPath( tab.getPath() ); |
| | + |
| | + final Processor<String> processor = getProcessors().computeIfAbsent( |
| | + tab, p -> createProcessors( tab ) |
| | + ); |
| | + |
| | + try { |
| | + updateCaretStatus( tab ); |
| | + processChain( processor, tab.getEditorText() ); |
| | + scrollToCaret(); |
| | + } catch( final Exception ex ) { |
| | + clue( ex ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + private void processActiveTab() { |
| | process( getActiveFileEditorTab() ); |
| | } |