| | consumer.accept( null ); |
| | } catch( final Exception ex ) { |
| | - error( ex ); |
| | - } |
| | - } |
| | - } ); |
| | - |
| | - Val.flatMap( node.sceneProperty(), Scene::windowProperty ) |
| | - .flatMap( Window::showingProperty ) |
| | - .addListener( listener ); |
| | - } |
| | - |
| | - private void scrollToParagraph( final int id ) { |
| | - scrollToParagraph( id, false ); |
| | - } |
| | - |
| | - /** |
| | - * @param id The paragraph to scroll to, will be approximated if it doesn't |
| | - * exist. |
| | - * @param force {@code true} means to force scrolling immediately, which |
| | - * should only be attempted when it is known that the document |
| | - * has been fully rendered. Otherwise the internal map of ID |
| | - * attributes will be incomplete and scrolling will flounder. |
| | - */ |
| | - private void scrollToParagraph( final int id, final boolean force ) { |
| | - synchronized( mMutex ) { |
| | - final var previewPane = getPreviewPane(); |
| | - final var scrollPane = previewPane.getScrollPane(); |
| | - final int approxId = getActiveEditorPane().approximateParagraphId( id ); |
| | - |
| | - if( force ) { |
| | - previewPane.scrollTo( approxId ); |
| | - } |
| | - else { |
| | - previewPane.tryScrollTo( approxId ); |
| | - } |
| | - |
| | - scrollPane.repaint(); |
| | - } |
| | - } |
| | - |
| | - private void updateVariableNameInjector( final FileEditorTab tab ) { |
| | - getVariableNameInjector().addListener( tab ); |
| | - } |
| | - |
| | - /** |
| | - * 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 { |
| | - processChain( processor, tab.getEditorText() ); |
| | - } catch( final Exception ex ) { |
| | - error( ex ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Executes the processing chain, operating on the given string. |
| | - * |
| | - * @param handler The first processor in the chain to call. |
| | - * @param text The initial value of the text to process. |
| | - * @return The final value of the text that was processed by the chain. |
| | - */ |
| | - private String processChain( Processor<String> handler, String text ) { |
| | - while( handler != null && text != null ) { |
| | - text = handler.apply( text ); |
| | - handler = handler.next(); |
| | - } |
| | - |
| | - return text; |
| | - } |
| | - |
| | - private void renderActiveTab() { |
| | - process( getActiveFileEditorTab() ); |
| | - } |
| | - |
| | - /** |
| | - * Called when a definition source is opened. |
| | - * |
| | - * @param path Path to the definition source that was opened. |
| | - */ |
| | - private void openDefinitions( final Path path ) { |
| | - try { |
| | - final var ds = createDefinitionSource( path ); |
| | - setDefinitionSource( ds ); |
| | - |
| | - final var prefs = getUserPreferences(); |
| | - prefs.definitionPathProperty().setValue( path.toFile() ); |
| | - prefs.save(); |
| | - |
| | - final var tooltipPath = new Tooltip( path.toString() ); |
| | - tooltipPath.setShowDelay( Duration.millis( 200 ) ); |
| | - |
| | - final var pane = getDefinitionPane(); |
| | - pane.update( ds ); |
| | - pane.addTreeChangeHandler( mTreeHandler ); |
| | - pane.addKeyEventHandler( mDefinitionKeyHandler ); |
| | - pane.filenameProperty().setValue( path.getFileName().toString() ); |
| | - pane.setTooltip( tooltipPath ); |
| | - |
| | - interpolateResolvedMap(); |
| | - } catch( final Exception ex ) { |
| | - error( ex ); |
| | - } |
| | - } |
| | - |
| | - private void exportDefinitions( final Path path ) { |
| | - try { |
| | - final var pane = getDefinitionPane(); |
| | - final var root = pane.getTreeView().getRoot(); |
| | - final var problemChild = pane.isTreeWellFormed(); |
| | - |
| | - if( problemChild == null ) { |
| | - getDefinitionSource().getTreeAdapter().export( root, path ); |
| | - getNotifier().clear(); |
| | - } |
| | - else { |
| | - error( get( "yaml.error.tree.form", problemChild.getValue() ) ); |
| | - } |
| | - } catch( final Exception ex ) { |
| | - error( ex ); |
| | - } |
| | - } |
| | - |
| | - private void interpolateResolvedMap() { |
| | - final var treeMap = getDefinitionPane().toMap(); |
| | - final var map = new HashMap<>( treeMap ); |
| | - MapInterpolator.interpolate( map ); |
| | - |
| | - getResolvedMap().clear(); |
| | - getResolvedMap().putAll( map ); |
| | - } |
| | - |
| | - private void initDefinitionPane() { |
| | - openDefinitions( getDefinitionPath() ); |
| | - } |
| | - |
| | - /** |
| | - * Called when an exception occurs that warrants the user's attention. |
| | - * |
| | - * @param ex The exception with a message that the user should know about. |
| | - */ |
| | - private void error( final Exception ex ) { |
| | - getNotifier().notify( ex ); |
| | - } |
| | - |
| | - private void error( final String msg ) { |
| | - getNotifier().notify( msg ); |
| | - } |
| | - |
| | - //---- File actions ------------------------------------------------------- |
| | - |
| | - /** |
| | - * Called when an {@link Observable} instance has changed. This is called |
| | - * by both the {@link Snitch} service and the notify service. The @link |
| | - * Snitch} service can be called for different file types, including |
| | - * {@link DefinitionSource} instances. |
| | - * |
| | - * @param observable The observed instance. |
| | - * @param value The noteworthy item. |
| | - */ |
| | - @Override |
| | - public void update( final Observable observable, final Object value ) { |
| | - if( value != null ) { |
| | - if( observable instanceof Snitch && value instanceof Path ) { |
| | - updateSelectedTab(); |
| | - } |
| | - else if( observable instanceof Notifier && value instanceof String ) { |
| | - updateStatusBar( (String) value ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Updates the status bar to show the given message. |
| | - * |
| | - * @param s The message to show in the status bar. |
| | - */ |
| | - private void updateStatusBar( final String s ) { |
| | - runLater( |
| | - () -> { |
| | - final int index = s.indexOf( '\n' ); |
| | - final String message = s.substring( |
| | - 0, index > 0 ? index : s.length() ); |
| | - |
| | - getStatusBar().setText( message ); |
| | - } |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * Called when a file has been modified. |
| | - */ |
| | - private void updateSelectedTab() { |
| | - rerender(); |
| | - } |
| | - |
| | - /** |
| | - * After resetting the processors, they will refresh anew to be up-to-date |
| | - * with the files (text and definition) currently loaded into the editor. |
| | - */ |
| | - private void resetProcessors() { |
| | - getProcessors().clear(); |
| | - } |
| | - |
| | - //---- File actions ------------------------------------------------------- |
| | - |
| | - private void fileNew() { |
| | - getFileEditorPane().newEditor(); |
| | - } |
| | - |
| | - private void fileOpen() { |
| | - getFileEditorPane().openFileDialog(); |
| | - } |
| | - |
| | - private void fileClose() { |
| | - getFileEditorPane().closeEditor( getActiveFileEditorTab(), true ); |
| | - } |
| | - |
| | - /** |
| | - * TODO: Upon closing, first remove the tab change listeners. (There's no |
| | - * need to re-render each tab when all are being closed.) |
| | - */ |
| | - private void fileCloseAll() { |
| | - getFileEditorPane().closeAllEditors(); |
| | - } |
| | - |
| | - private void fileSave() { |
| | - getFileEditorPane().saveEditor( getActiveFileEditorTab() ); |
| | - } |
| | - |
| | - private void fileSaveAs() { |
| | - final FileEditorTab editor = getActiveFileEditorTab(); |
| | - getFileEditorPane().saveEditorAs( editor ); |
| | - getProcessors().remove( editor ); |
| | - |
| | - try { |
| | - process( editor ); |
| | - } catch( final Exception ex ) { |
| | - error( ex ); |
| | - } |
| | - } |
| | - |
| | - private void fileSaveAll() { |
| | - getFileEditorPane().saveAllEditors(); |
| | - } |
| | - |
| | - private void fileExit() { |
| | - final Window window = getWindow(); |
| | - fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); |
| | - } |
| | - |
| | - //---- Edit actions ------------------------------------------------------- |
| | - |
| | - /** |
| | - * Transform the Markdown into HTML then copy that HTML into the copy |
| | - * buffer. |
| | - */ |
| | - private void copyHtml() { |
| | - final var markdown = getActiveEditorPane().getText(); |
| | - final var processors = createProcessorFactory().createProcessors( |
| | - getActiveFileEditorTab() |
| | - ); |
| | - |
| | - final var chain = processors.remove( HtmlPreviewProcessor.class ); |
| | - |
| | - final String html = processChain( chain, markdown ); |
| | - |
| | - final Clipboard clipboard = Clipboard.getSystemClipboard(); |
| | - final ClipboardContent content = new ClipboardContent(); |
| | - content.putString( html ); |
| | - clipboard.setContent( content ); |
| | - } |
| | - |
| | - /** |
| | - * Used to find text in the active file editor window. |
| | - */ |
| | - private void editFind() { |
| | - final TextField input = getFindTextField(); |
| | - getStatusBar().setGraphic( input ); |
| | - input.requestFocus(); |
| | - } |
| | - |
| | - public void editFindNext() { |
| | - getActiveFileEditorTab().searchNext( getFindTextField().getText() ); |
| | - } |
| | - |
| | - public void editPreferences() { |
| | - getUserPreferences().show(); |
| | - } |
| | - |
| | - //---- Insert actions ----------------------------------------------------- |
| | - |
| | - /** |
| | - * Delegates to the active editor to handle wrapping the current text |
| | - * selection with leading and trailing strings. |
| | - * |
| | - * @param leading The string to put before the selection. |
| | - * @param trailing The string to put after the selection. |
| | - */ |
| | - private void insertMarkdown( |
| | - final String leading, final String trailing ) { |
| | - getActiveEditorPane().surroundSelection( leading, trailing ); |
| | - } |
| | - |
| | - private void insertMarkdown( |
| | - final String leading, final String trailing, final String hint ) { |
| | - getActiveEditorPane().surroundSelection( leading, trailing, hint ); |
| | - } |
| | - |
| | - //---- View actions ------------------------------------------------------- |
| | - |
| | - private void viewRefresh() { |
| | - rerender(); |
| | - } |
| | - |
| | - //---- Help actions ------------------------------------------------------- |
| | - |
| | - private void helpAbout() { |
| | - final Alert alert = new Alert( AlertType.INFORMATION ); |
| | - alert.setTitle( get( "Dialog.about.title" ) ); |
| | - alert.setHeaderText( get( "Dialog.about.header" ) ); |
| | - alert.setContentText( get( "Dialog.about.content" ) ); |
| | - alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); |
| | - alert.initOwner( getWindow() ); |
| | - |
| | - alert.showAndWait(); |
| | - } |
| | - |
| | - //---- Member creators ---------------------------------------------------- |
| | - |
| | - private SpellChecker createSpellChecker() { |
| | - try { |
| | - final Collection<String> lexicon = readLexicon( "en.txt" ); |
| | - return SymSpellSpeller.forLexicon( lexicon ); |
| | - } catch( final Exception ex ) { |
| | - error( ex ); |
| | - return new PermissiveSpeller(); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Factory to create processors that are suited to different file types. |
| | - * |
| | - * @param tab The tab that is subjected to processing. |
| | - * @return A processor suited to the file type specified by the tab's path. |
| | - */ |
| | - private Processor<String> createProcessors( final FileEditorTab tab ) { |
| | - return createProcessorFactory().createProcessors( tab ); |
| | - } |
| | - |
| | - private ProcessorFactory createProcessorFactory() { |
| | - return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); |
| | - } |
| | - |
| | - private HTMLPreviewPane createHTMLPreviewPane() { |
| | - return new HTMLPreviewPane(); |
| | - } |
| | - |
| | - private DefinitionSource createDefaultDefinitionSource() { |
| | - return new YamlDefinitionSource( getDefinitionPath() ); |
| | - } |
| | - |
| | - private DefinitionSource createDefinitionSource( final Path path ) { |
| | - try { |
| | - return createDefinitionFactory().createDefinitionSource( path ); |
| | - } catch( final Exception ex ) { |
| | - error( ex ); |
| | - return createDefaultDefinitionSource(); |
| | - } |
| | - } |
| | - |
| | - private TextField createFindTextField() { |
| | - return new TextField(); |
| | - } |
| | - |
| | - private DefinitionFactory createDefinitionFactory() { |
| | - return new DefinitionFactory(); |
| | - } |
| | - |
| | - private StatusBar createStatusBar() { |
| | - return new StatusBar(); |
| | - } |
| | - |
| | - private Scene createScene() { |
| | - final SplitPane splitPane = new SplitPane( |
| | - getDefinitionPane(), |
| | - getFileEditorPane(), |
| | - getPreviewPane() ); |
| | - |
| | - splitPane.setDividerPositions( |
| | - getFloat( K_PANE_SPLIT_DEFINITION, .22f ), |
| | - getFloat( K_PANE_SPLIT_EDITOR, .60f ), |
| | - getFloat( K_PANE_SPLIT_PREVIEW, .18f ) ); |
| | - |
| | - getDefinitionPane().prefHeightProperty() |
| | - .bind( splitPane.heightProperty() ); |
| | - |
| | - final BorderPane borderPane = new BorderPane(); |
| | - borderPane.setPrefSize( 1280, 800 ); |
| | - borderPane.setTop( createMenuBar() ); |
| | - borderPane.setBottom( getStatusBar() ); |
| | - borderPane.setCenter( splitPane ); |
| | - |
| | - final VBox statusBar = new VBox(); |
| | - statusBar.setAlignment( Pos.BASELINE_CENTER ); |
| | - statusBar.getChildren().add( getLineNumberText() ); |
| | - getStatusBar().getRightItems().add( statusBar ); |
| | - |
| | - // Force preview pane refresh on Windows. |
| | - if( SystemUtils.IS_OS_WINDOWS ) { |
| | - splitPane.getDividers().get( 1 ).positionProperty().addListener( |
| | - ( l, oValue, nValue ) -> runLater( |
| | - () -> getPreviewPane().getScrollPane().repaint() |
| | - ) |
| | - ); |
| | - } |
| | - |
| | - return new Scene( borderPane ); |
| | - } |
| | - |
| | - private Text createLineNumberText() { |
| | - return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) ); |
| | - } |
| | - |
| | - private Node createMenuBar() { |
| | - final BooleanBinding activeFileEditorIsNull = |
| | - getFileEditorPane().activeFileEditorProperty().isNull(); |
| | - |
| | - // File actions |
| | - final Action fileNewAction = new ActionBuilder() |
| | - .setText( "Main.menu.file.new" ) |
| | - .setAccelerator( "Shortcut+N" ) |
| | - .setIcon( FILE_ALT ) |
| | - .setAction( e -> fileNew() ) |
| | - .build(); |
| | - final Action fileOpenAction = new ActionBuilder() |
| | - .setText( "Main.menu.file.open" ) |
| | - .setAccelerator( "Shortcut+O" ) |
| | - .setIcon( FOLDER_OPEN_ALT ) |
| | - .setAction( e -> fileOpen() ) |
| | - .build(); |
| | - final Action fileCloseAction = new ActionBuilder() |
| | - .setText( "Main.menu.file.close" ) |
| | - .setAccelerator( "Shortcut+W" ) |
| | - .setAction( e -> fileClose() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action fileCloseAllAction = new ActionBuilder() |
| | - .setText( "Main.menu.file.close_all" ) |
| | - .setAction( e -> fileCloseAll() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action fileSaveAction = new ActionBuilder() |
| | - .setText( "Main.menu.file.save" ) |
| | - .setAccelerator( "Shortcut+S" ) |
| | - .setIcon( FLOPPY_ALT ) |
| | - .setAction( e -> fileSave() ) |
| | - .setDisable( createActiveBooleanProperty( |
| | - FileEditorTab::modifiedProperty ).not() ) |
| | - .build(); |
| | - final Action fileSaveAsAction = new ActionBuilder() |
| | - .setText( "Main.menu.file.save_as" ) |
| | - .setAction( e -> fileSaveAs() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action fileSaveAllAction = new ActionBuilder() |
| | - .setText( "Main.menu.file.save_all" ) |
| | - .setAccelerator( "Shortcut+Shift+S" ) |
| | - .setAction( e -> fileSaveAll() ) |
| | - .setDisable( Bindings.not( |
| | - getFileEditorPane().anyFileEditorModifiedProperty() ) ) |
| | - .build(); |
| | - final Action fileExitAction = new ActionBuilder() |
| | - .setText( "Main.menu.file.exit" ) |
| | - .setAction( e -> fileExit() ) |
| | - .build(); |
| | - |
| | - // Edit actions |
| | - final Action editCopyHtmlAction = new ActionBuilder() |
| | - .setText( Messages.get( "Main.menu.edit.copy.html" ) ) |
| | - .setIcon( HTML5 ) |
| | - .setAction( e -> copyHtml() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - |
| | - final Action editUndoAction = new ActionBuilder() |
| | - .setText( "Main.menu.edit.undo" ) |
| | - .setAccelerator( "Shortcut+Z" ) |
| | - .setIcon( UNDO ) |
| | - .setAction( e -> getActiveEditorPane().undo() ) |
| | - .setDisable( createActiveBooleanProperty( |
| | - FileEditorTab::canUndoProperty ).not() ) |
| | - .build(); |
| | - final Action editRedoAction = new ActionBuilder() |
| | - .setText( "Main.menu.edit.redo" ) |
| | - .setAccelerator( "Shortcut+Y" ) |
| | - .setIcon( REPEAT ) |
| | - .setAction( e -> getActiveEditorPane().redo() ) |
| | - .setDisable( createActiveBooleanProperty( |
| | - FileEditorTab::canRedoProperty ).not() ) |
| | - .build(); |
| | - |
| | - final Action editCutAction = new ActionBuilder() |
| | - .setText( Messages.get( "Main.menu.edit.cut" ) ) |
| | - .setAccelerator( "Shortcut+X" ) |
| | - .setIcon( CUT ) |
| | - .setAction( e -> getActiveEditorPane().cut() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action editCopyAction = new ActionBuilder() |
| | - .setText( Messages.get( "Main.menu.edit.copy" ) ) |
| | - .setAccelerator( "Shortcut+C" ) |
| | - .setIcon( COPY ) |
| | - .setAction( e -> getActiveEditorPane().copy() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action editPasteAction = new ActionBuilder() |
| | - .setText( Messages.get( "Main.menu.edit.paste" ) ) |
| | - .setAccelerator( "Shortcut+V" ) |
| | - .setIcon( PASTE ) |
| | - .setAction( e -> getActiveEditorPane().paste() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action editSelectAllAction = new ActionBuilder() |
| | - .setText( Messages.get( "Main.menu.edit.selectAll" ) ) |
| | - .setAccelerator( "Shortcut+A" ) |
| | - .setAction( e -> getActiveEditorPane().selectAll() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - |
| | - final Action editFindAction = new ActionBuilder() |
| | - .setText( "Main.menu.edit.find" ) |
| | - .setAccelerator( "Ctrl+F" ) |
| | - .setIcon( SEARCH ) |
| | - .setAction( e -> editFind() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action editFindNextAction = new ActionBuilder() |
| | - .setText( "Main.menu.edit.find.next" ) |
| | - .setAccelerator( "F3" ) |
| | - .setIcon( null ) |
| | - .setAction( e -> editFindNext() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action editPreferencesAction = new ActionBuilder() |
| | - .setText( "Main.menu.edit.preferences" ) |
| | - .setAccelerator( "Ctrl+Alt+S" ) |
| | - .setAction( e -> editPreferences() ) |
| | - .build(); |
| | - |
| | - // Insert actions |
| | - final Action insertBoldAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.bold" ) |
| | - .setAccelerator( "Shortcut+B" ) |
| | - .setIcon( BOLD ) |
| | - .setAction( e -> insertMarkdown( "**", "**" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertItalicAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.italic" ) |
| | - .setAccelerator( "Shortcut+I" ) |
| | - .setIcon( ITALIC ) |
| | - .setAction( e -> insertMarkdown( "*", "*" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertSuperscriptAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.superscript" ) |
| | - .setAccelerator( "Shortcut+[" ) |
| | - .setIcon( SUPERSCRIPT ) |
| | - .setAction( e -> insertMarkdown( "^", "^" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertSubscriptAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.subscript" ) |
| | - .setAccelerator( "Shortcut+]" ) |
| | - .setIcon( SUBSCRIPT ) |
| | - .setAction( e -> insertMarkdown( "~", "~" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertStrikethroughAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.strikethrough" ) |
| | - .setAccelerator( "Shortcut+T" ) |
| | - .setIcon( STRIKETHROUGH ) |
| | - .setAction( e -> insertMarkdown( "~~", "~~" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertBlockquoteAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.blockquote" ) |
| | - .setAccelerator( "Ctrl+Q" ) |
| | - .setIcon( QUOTE_LEFT ) |
| | - .setAction( e -> insertMarkdown( "\n\n> ", "" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertCodeAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.code" ) |
| | - .setAccelerator( "Shortcut+K" ) |
| | - .setIcon( CODE ) |
| | - .setAction( e -> insertMarkdown( "`", "`" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertFencedCodeBlockAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.fenced_code_block" ) |
| | - .setAccelerator( "Shortcut+Shift+K" ) |
| | - .setIcon( FILE_CODE_ALT ) |
| | - .setAction( e -> insertMarkdown( |
| | - "\n\n```\n", |
| | - "\n```\n\n", |
| | - get( "Main.menu.insert.fenced_code_block.prompt" ) ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertLinkAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.link" ) |
| | - .setAccelerator( "Shortcut+L" ) |
| | - .setIcon( LINK ) |
| | - .setAction( e -> getActiveEditorPane().insertLink() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertImageAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.image" ) |
| | - .setAccelerator( "Shortcut+G" ) |
| | - .setIcon( PICTURE_ALT ) |
| | - .setAction( e -> getActiveEditorPane().insertImage() ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - |
| | - // Number of heading actions (H1 ... H3) |
| | - final int HEADINGS = 3; |
| | - final Action[] headings = new Action[ HEADINGS ]; |
| | - |
| | - for( int i = 1; i <= HEADINGS; i++ ) { |
| | - final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); |
| | - final String markup = String.format( "%n%n%s ", hashes ); |
| | - final String text = "Main.menu.insert.heading." + i; |
| | - final String accelerator = "Shortcut+" + i; |
| | - final String prompt = text + ".prompt"; |
| | - |
| | - headings[ i - 1 ] = new ActionBuilder() |
| | - .setText( text ) |
| | - .setAccelerator( accelerator ) |
| | - .setIcon( HEADER ) |
| | - .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - } |
| | - |
| | - final Action insertUnorderedListAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.unordered_list" ) |
| | - .setAccelerator( "Shortcut+U" ) |
| | - .setIcon( LIST_UL ) |
| | - .setAction( e -> insertMarkdown( "\n\n* ", "" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertOrderedListAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.ordered_list" ) |
| | - .setAccelerator( "Shortcut+Shift+O" ) |
| | - .setIcon( LIST_OL ) |
| | - .setAction( e -> insertMarkdown( |
| | - "\n\n1. ", "" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - final Action insertHorizontalRuleAction = new ActionBuilder() |
| | - .setText( "Main.menu.insert.horizontal_rule" ) |
| | - .setAccelerator( "Shortcut+H" ) |
| | - .setAction( e -> insertMarkdown( |
| | - "\n\n---\n\n", "" ) ) |
| | - .setDisable( activeFileEditorIsNull ) |
| | - .build(); |
| | - |
| | - // View actions |
| | - final Action viewRefreshAction = new ActionBuilder() |
| | - .setText( "Main.menu.view.refresh" ) |
| | - .setAccelerator( "F5" ) |
| | - .setAction( e -> viewRefresh() ) |
| | - .build(); |
| | - |
| | - // Help actions |
| | - final Action helpAboutAction = new ActionBuilder() |
| | - .setText( "Main.menu.help.about" ) |
| | - .setAction( e -> helpAbout() ) |
| | - .build(); |
| | - |
| | - //---- MenuBar ---- |
| | - final Menu fileMenu = ActionUtils.createMenu( |
| | - get( "Main.menu.file" ), |
| | - fileNewAction, |
| | - fileOpenAction, |
| | - null, |
| | - fileCloseAction, |
| | - fileCloseAllAction, |
| | - null, |
| | - fileSaveAction, |
| | - fileSaveAsAction, |
| | - fileSaveAllAction, |
| | - null, |
| | - fileExitAction ); |
| | - |
| | - final Menu editMenu = ActionUtils.createMenu( |
| | - get( "Main.menu.edit" ), |
| | - editCopyHtmlAction, |
| | - null, |
| | - editUndoAction, |
| | - editRedoAction, |
| | - null, |
| | - editCutAction, |
| | - editCopyAction, |
| | - editPasteAction, |
| | - editSelectAllAction, |
| | - null, |
| | - editFindAction, |
| | - editFindNextAction, |
| | - null, |
| | - editPreferencesAction ); |
| | - |
| | - final Menu insertMenu = ActionUtils.createMenu( |
| | - get( "Main.menu.insert" ), |
| | - insertBoldAction, |
| | - insertItalicAction, |
| | - insertSuperscriptAction, |
| | - insertSubscriptAction, |
| | - insertStrikethroughAction, |
| | - insertBlockquoteAction, |
| | - insertCodeAction, |
| | - insertFencedCodeBlockAction, |
| | - null, |
| | - insertLinkAction, |
| | - insertImageAction, |
| | - null, |
| | - headings[ 0 ], |
| | - headings[ 1 ], |
| | - headings[ 2 ], |
| | - null, |
| | - insertUnorderedListAction, |
| | - insertOrderedListAction, |
| | - insertHorizontalRuleAction |
| | - ); |
| | - |
| | - final Menu viewMenu = ActionUtils.createMenu( |
| | - get( "Main.menu.view" ), |
| | - viewRefreshAction ); |
| | - |
| | - final Menu helpMenu = ActionUtils.createMenu( |
| | - get( "Main.menu.help" ), |
| | - helpAboutAction ); |
| | - |
| | - final MenuBar menuBar = new MenuBar( |
| | - fileMenu, |
| | - editMenu, |
| | - insertMenu, |
| | - viewMenu, |
| | - helpMenu ); |
| | - |
| | - //---- ToolBar ---- |
| | - final ToolBar toolBar = ActionUtils.createToolBar( |
| | - fileNewAction, |
| | - fileOpenAction, |
| | - fileSaveAction, |
| | - null, |
| | - editUndoAction, |
| | - editRedoAction, |
| | - editCutAction, |
| | - editCopyAction, |
| | - editPasteAction, |
| | - null, |
| | - insertBoldAction, |
| | - insertItalicAction, |
| | - insertSuperscriptAction, |
| | - insertSubscriptAction, |
| | - insertBlockquoteAction, |
| | - insertCodeAction, |
| | - insertFencedCodeBlockAction, |
| | - null, |
| | - insertLinkAction, |
| | - insertImageAction, |
| | - null, |
| | - headings[ 0 ], |
| | - null, |
| | - insertUnorderedListAction, |
| | - insertOrderedListAction ); |
| | - |
| | - return new VBox( menuBar, toolBar ); |
| | - } |
| | - |
| | - /** |
| | - * Creates a boolean property that is bound to another boolean value of the |
| | - * active editor. |
| | - */ |
| | - private BooleanProperty createActiveBooleanProperty( |
| | - final Function<FileEditorTab, ObservableBooleanValue> func ) { |
| | - |
| | - final BooleanProperty b = new SimpleBooleanProperty(); |
| | - final FileEditorTab tab = getActiveFileEditorTab(); |
| | - |
| | - if( tab != null ) { |
| | - b.bind( func.apply( tab ) ); |
| | - } |
| | - |
| | - getFileEditorPane().activeFileEditorProperty().addListener( |
| | - ( observable, oldFileEditor, newFileEditor ) -> { |
| | - b.unbind(); |
| | - |
| | - if( newFileEditor == null ) { |
| | - b.set( false ); |
| | - } |
| | - else { |
| | - b.bind( func.apply( newFileEditor ) ); |
| | - } |
| | - } |
| | - ); |
| | - |
| | - return b; |
| | - } |
| | - |
| | - //---- Convenience accessors ---------------------------------------------- |
| | - |
| | - private Preferences getPreferences() { |
| | - return sOptions.getState(); |
| | - } |
| | - |
| | - private int getCurrentParagraphIndex() { |
| | - return getActiveEditorPane().getCurrentParagraphIndex(); |
| | - } |
| | - |
| | - private float getFloat( final String key, final float defaultValue ) { |
| | - return getPreferences().getFloat( key, defaultValue ); |
| | - } |
| | - |
| | - public Window getWindow() { |
| | - return getScene().getWindow(); |
| | - } |
| | - |
| | - private MarkdownEditorPane getActiveEditorPane() { |
| | - return getActiveFileEditorTab().getEditorPane(); |
| | - } |
| | - |
| | - private FileEditorTab getActiveFileEditorTab() { |
| | - return getFileEditorPane().getActiveFileEditor(); |
| | - } |
| | - |
| | - //---- Member accessors --------------------------------------------------- |
| | - |
| | - protected Scene getScene() { |
| | - return mScene; |
| | - } |
| | - |
| | - private SpellChecker getSpellChecker() { |
| | - return mSpellChecker; |
| | - } |
| | - |
| | - private Map<FileEditorTab, Processor<String>> getProcessors() { |
| | - return mProcessors; |
| | - } |
| | - |
| | - private FileEditorTabPane getFileEditorPane() { |
| | - return mFileEditorPane; |
| | - } |
| | - |
| | - private HTMLPreviewPane getPreviewPane() { |
| | - return mPreviewPane; |
| | - } |
| | - |
| | - private void setDefinitionSource( |
| | - final DefinitionSource definitionSource ) { |
| | - assert definitionSource != null; |
| | - mDefinitionSource = definitionSource; |
| | - } |
| | - |
| | - private DefinitionSource getDefinitionSource() { |
| | - return mDefinitionSource; |
| | - } |
| | - |
| | - private DefinitionPane getDefinitionPane() { |
| | - return mDefinitionPane; |
| | - } |
| | - |
| | - private Text getLineNumberText() { |
| | - return mLineNumberText; |
| | - } |
| | - |
| | - private StatusBar getStatusBar() { |
| | - return mStatusBar; |
| | - } |
| | - |
| | - private TextField getFindTextField() { |
| | - return mFindTextField; |
| | - } |
| | - |
| | - private DefinitionNameInjector getVariableNameInjector() { |
| | - return mVariableNameInjector; |
| | - } |
| | - |
| | - /** |
| | - * Returns the variable map of interpolated definitions. |
| | - * |
| | - * @return A map to help dereference variables. |
| | - */ |
| | - private Map<String, String> getResolvedMap() { |
| | - return mResolvedMap; |
| | - } |
| | - |
| | - private Notifier getNotifier() { |
| | + alert( ex ); |
| | + } |
| | + } |
| | + } ); |
| | + |
| | + Val.flatMap( node.sceneProperty(), Scene::windowProperty ) |
| | + .flatMap( Window::showingProperty ) |
| | + .addListener( listener ); |
| | + } |
| | + |
| | + private void scrollToParagraph( final int id ) { |
| | + scrollToParagraph( id, false ); |
| | + } |
| | + |
| | + /** |
| | + * @param id The paragraph to scroll to, will be approximated if it doesn't |
| | + * exist. |
| | + * @param force {@code true} means to force scrolling immediately, which |
| | + * should only be attempted when it is known that the document |
| | + * has been fully rendered. Otherwise the internal map of ID |
| | + * attributes will be incomplete and scrolling will flounder. |
| | + */ |
| | + private void scrollToParagraph( final int id, final boolean force ) { |
| | + synchronized( mMutex ) { |
| | + final var previewPane = getPreviewPane(); |
| | + final var scrollPane = previewPane.getScrollPane(); |
| | + final int approxId = getActiveEditorPane().approximateParagraphId( id ); |
| | + |
| | + if( force ) { |
| | + previewPane.scrollTo( approxId ); |
| | + } |
| | + else { |
| | + previewPane.tryScrollTo( approxId ); |
| | + } |
| | + |
| | + scrollPane.repaint(); |
| | + } |
| | + } |
| | + |
| | + private void updateVariableNameInjector( final FileEditorTab tab ) { |
| | + getVariableNameInjector().addListener( tab ); |
| | + } |
| | + |
| | + /** |
| | + * 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 { |
| | + processChain( processor, tab.getEditorText() ); |
| | + } catch( final Exception ex ) { |
| | + alert( ex ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Executes the processing chain, operating on the given string. |
| | + * |
| | + * @param handler The first processor in the chain to call. |
| | + * @param text The initial value of the text to process. |
| | + * @return The final value of the text that was processed by the chain. |
| | + */ |
| | + private String processChain( Processor<String> handler, String text ) { |
| | + while( handler != null && text != null ) { |
| | + text = handler.apply( text ); |
| | + handler = handler.next(); |
| | + } |
| | + |
| | + return text; |
| | + } |
| | + |
| | + private void renderActiveTab() { |
| | + process( getActiveFileEditorTab() ); |
| | + } |
| | + |
| | + /** |
| | + * Called when a definition source is opened. |
| | + * |
| | + * @param path Path to the definition source that was opened. |
| | + */ |
| | + private void openDefinitions( final Path path ) { |
| | + try { |
| | + final var ds = createDefinitionSource( path ); |
| | + setDefinitionSource( ds ); |
| | + |
| | + final var prefs = getUserPreferences(); |
| | + prefs.definitionPathProperty().setValue( path.toFile() ); |
| | + prefs.save(); |
| | + |
| | + final var tooltipPath = new Tooltip( path.toString() ); |
| | + tooltipPath.setShowDelay( Duration.millis( 200 ) ); |
| | + |
| | + final var pane = getDefinitionPane(); |
| | + pane.update( ds ); |
| | + pane.addTreeChangeHandler( mTreeHandler ); |
| | + pane.addKeyEventHandler( mDefinitionKeyHandler ); |
| | + pane.filenameProperty().setValue( path.getFileName().toString() ); |
| | + pane.setTooltip( tooltipPath ); |
| | + |
| | + interpolateResolvedMap(); |
| | + } catch( final Exception ex ) { |
| | + alert( ex ); |
| | + } |
| | + } |
| | + |
| | + private void exportDefinitions( final Path path ) { |
| | + try { |
| | + final var pane = getDefinitionPane(); |
| | + final var root = pane.getTreeView().getRoot(); |
| | + final var problemChild = pane.isTreeWellFormed(); |
| | + |
| | + if( problemChild == null ) { |
| | + getDefinitionSource().getTreeAdapter().export( root, path ); |
| | + clearAlert(); |
| | + } |
| | + else { |
| | + alert( get( "yaml.error.tree.form", problemChild.getValue() ) ); |
| | + } |
| | + } catch( final Exception ex ) { |
| | + alert( ex ); |
| | + } |
| | + } |
| | + |
| | + private void interpolateResolvedMap() { |
| | + final var treeMap = getDefinitionPane().toMap(); |
| | + final var map = new HashMap<>( treeMap ); |
| | + MapInterpolator.interpolate( map ); |
| | + |
| | + getResolvedMap().clear(); |
| | + getResolvedMap().putAll( map ); |
| | + } |
| | + |
| | + private void initDefinitionPane() { |
| | + openDefinitions( getDefinitionPath() ); |
| | + } |
| | + |
| | + private static void clearAlert() { |
| | + getNotifier().clear(); |
| | + } |
| | + |
| | + /** |
| | + * Called when an exception occurs that warrants the user's attention. |
| | + * |
| | + * @param ex The exception with a message that the user should know about. |
| | + */ |
| | + private static void alert( final Exception ex ) { |
| | + getNotifier().alert( ex ); |
| | + } |
| | + |
| | + private static void alert( final String msg ) { |
| | + getNotifier().alert( msg ); |
| | + } |
| | + |
| | + //---- File actions ------------------------------------------------------- |
| | + |
| | + /** |
| | + * Called when an {@link Observable} instance has changed. This is called |
| | + * by both the {@link Snitch} service and the notify service. The @link |
| | + * Snitch} service can be called for different file types, including |
| | + * {@link DefinitionSource} instances. |
| | + * |
| | + * @param observable The observed instance. |
| | + * @param value The noteworthy item. |
| | + */ |
| | + @Override |
| | + public void update( final Observable observable, final Object value ) { |
| | + if( value != null ) { |
| | + if( observable instanceof Snitch && value instanceof Path ) { |
| | + updateSelectedTab(); |
| | + } |
| | + else if( observable instanceof Notifier && value instanceof String ) { |
| | + updateStatusBar( (String) value ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Updates the status bar to show the given message. |
| | + * |
| | + * @param s The message to show in the status bar. |
| | + */ |
| | + private void updateStatusBar( final String s ) { |
| | + runLater( |
| | + () -> { |
| | + final int index = s.indexOf( '\n' ); |
| | + final String message = s.substring( |
| | + 0, index > 0 ? index : s.length() ); |
| | + |
| | + getStatusBar().setText( message ); |
| | + } |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * Called when a file has been modified. |
| | + */ |
| | + private void updateSelectedTab() { |
| | + rerender(); |
| | + } |
| | + |
| | + /** |
| | + * After resetting the processors, they will refresh anew to be up-to-date |
| | + * with the files (text and definition) currently loaded into the editor. |
| | + */ |
| | + private void resetProcessors() { |
| | + getProcessors().clear(); |
| | + } |
| | + |
| | + //---- File actions ------------------------------------------------------- |
| | + |
| | + private void fileNew() { |
| | + getFileEditorPane().newEditor(); |
| | + } |
| | + |
| | + private void fileOpen() { |
| | + getFileEditorPane().openFileDialog(); |
| | + } |
| | + |
| | + private void fileClose() { |
| | + getFileEditorPane().closeEditor( getActiveFileEditorTab(), true ); |
| | + } |
| | + |
| | + /** |
| | + * TODO: Upon closing, first remove the tab change listeners. (There's no |
| | + * need to re-render each tab when all are being closed.) |
| | + */ |
| | + private void fileCloseAll() { |
| | + getFileEditorPane().closeAllEditors(); |
| | + } |
| | + |
| | + private void fileSave() { |
| | + getFileEditorPane().saveEditor( getActiveFileEditorTab() ); |
| | + } |
| | + |
| | + private void fileSaveAs() { |
| | + final FileEditorTab editor = getActiveFileEditorTab(); |
| | + getFileEditorPane().saveEditorAs( editor ); |
| | + getProcessors().remove( editor ); |
| | + |
| | + try { |
| | + process( editor ); |
| | + } catch( final Exception ex ) { |
| | + alert( ex ); |
| | + } |
| | + } |
| | + |
| | + private void fileSaveAll() { |
| | + getFileEditorPane().saveAllEditors(); |
| | + } |
| | + |
| | + private void fileExit() { |
| | + final Window window = getWindow(); |
| | + fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); |
| | + } |
| | + |
| | + //---- Edit actions ------------------------------------------------------- |
| | + |
| | + /** |
| | + * Transform the Markdown into HTML then copy that HTML into the copy |
| | + * buffer. |
| | + */ |
| | + private void copyHtml() { |
| | + final var markdown = getActiveEditorPane().getText(); |
| | + final var processors = createProcessorFactory().createProcessors( |
| | + getActiveFileEditorTab() |
| | + ); |
| | + |
| | + final var chain = processors.remove( HtmlPreviewProcessor.class ); |
| | + |
| | + final String html = processChain( chain, markdown ); |
| | + |
| | + final Clipboard clipboard = Clipboard.getSystemClipboard(); |
| | + final ClipboardContent content = new ClipboardContent(); |
| | + content.putString( html ); |
| | + clipboard.setContent( content ); |
| | + } |
| | + |
| | + /** |
| | + * Used to find text in the active file editor window. |
| | + */ |
| | + private void editFind() { |
| | + final TextField input = getFindTextField(); |
| | + getStatusBar().setGraphic( input ); |
| | + input.requestFocus(); |
| | + } |
| | + |
| | + public void editFindNext() { |
| | + getActiveFileEditorTab().searchNext( getFindTextField().getText() ); |
| | + } |
| | + |
| | + public void editPreferences() { |
| | + getUserPreferences().show(); |
| | + } |
| | + |
| | + //---- Insert actions ----------------------------------------------------- |
| | + |
| | + /** |
| | + * Delegates to the active editor to handle wrapping the current text |
| | + * selection with leading and trailing strings. |
| | + * |
| | + * @param leading The string to put before the selection. |
| | + * @param trailing The string to put after the selection. |
| | + */ |
| | + private void insertMarkdown( |
| | + final String leading, final String trailing ) { |
| | + getActiveEditorPane().surroundSelection( leading, trailing ); |
| | + } |
| | + |
| | + private void insertMarkdown( |
| | + final String leading, final String trailing, final String hint ) { |
| | + getActiveEditorPane().surroundSelection( leading, trailing, hint ); |
| | + } |
| | + |
| | + //---- View actions ------------------------------------------------------- |
| | + |
| | + private void viewRefresh() { |
| | + rerender(); |
| | + } |
| | + |
| | + //---- Help actions ------------------------------------------------------- |
| | + |
| | + private void helpAbout() { |
| | + final Alert alert = new Alert( AlertType.INFORMATION ); |
| | + alert.setTitle( get( "Dialog.about.title" ) ); |
| | + alert.setHeaderText( get( "Dialog.about.header" ) ); |
| | + alert.setContentText( get( "Dialog.about.content" ) ); |
| | + alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); |
| | + alert.initOwner( getWindow() ); |
| | + |
| | + alert.showAndWait(); |
| | + } |
| | + |
| | + //---- Member creators ---------------------------------------------------- |
| | + |
| | + private SpellChecker createSpellChecker() { |
| | + try { |
| | + final Collection<String> lexicon = readLexicon( "en.txt" ); |
| | + return SymSpellSpeller.forLexicon( lexicon ); |
| | + } catch( final Exception ex ) { |
| | + alert( ex ); |
| | + return new PermissiveSpeller(); |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Factory to create processors that are suited to different file types. |
| | + * |
| | + * @param tab The tab that is subjected to processing. |
| | + * @return A processor suited to the file type specified by the tab's path. |
| | + */ |
| | + private Processor<String> createProcessors( final FileEditorTab tab ) { |
| | + return createProcessorFactory().createProcessors( tab ); |
| | + } |
| | + |
| | + private ProcessorFactory createProcessorFactory() { |
| | + return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); |
| | + } |
| | + |
| | + private HTMLPreviewPane createHTMLPreviewPane() { |
| | + return new HTMLPreviewPane(); |
| | + } |
| | + |
| | + private DefinitionSource createDefaultDefinitionSource() { |
| | + return new YamlDefinitionSource( getDefinitionPath() ); |
| | + } |
| | + |
| | + private DefinitionSource createDefinitionSource( final Path path ) { |
| | + try { |
| | + return createDefinitionFactory().createDefinitionSource( path ); |
| | + } catch( final Exception ex ) { |
| | + alert( ex ); |
| | + return createDefaultDefinitionSource(); |
| | + } |
| | + } |
| | + |
| | + private TextField createFindTextField() { |
| | + return new TextField(); |
| | + } |
| | + |
| | + private DefinitionFactory createDefinitionFactory() { |
| | + return new DefinitionFactory(); |
| | + } |
| | + |
| | + private StatusBar createStatusBar() { |
| | + return new StatusBar(); |
| | + } |
| | + |
| | + private Scene createScene() { |
| | + final SplitPane splitPane = new SplitPane( |
| | + getDefinitionPane(), |
| | + getFileEditorPane(), |
| | + getPreviewPane() ); |
| | + |
| | + splitPane.setDividerPositions( |
| | + getFloat( K_PANE_SPLIT_DEFINITION, .22f ), |
| | + getFloat( K_PANE_SPLIT_EDITOR, .60f ), |
| | + getFloat( K_PANE_SPLIT_PREVIEW, .18f ) ); |
| | + |
| | + getDefinitionPane().prefHeightProperty() |
| | + .bind( splitPane.heightProperty() ); |
| | + |
| | + final BorderPane borderPane = new BorderPane(); |
| | + borderPane.setPrefSize( 1280, 800 ); |
| | + borderPane.setTop( createMenuBar() ); |
| | + borderPane.setBottom( getStatusBar() ); |
| | + borderPane.setCenter( splitPane ); |
| | + |
| | + final VBox statusBar = new VBox(); |
| | + statusBar.setAlignment( Pos.BASELINE_CENTER ); |
| | + statusBar.getChildren().add( getLineNumberText() ); |
| | + getStatusBar().getRightItems().add( statusBar ); |
| | + |
| | + // Force preview pane refresh on Windows. |
| | + if( SystemUtils.IS_OS_WINDOWS ) { |
| | + splitPane.getDividers().get( 1 ).positionProperty().addListener( |
| | + ( l, oValue, nValue ) -> runLater( |
| | + () -> getPreviewPane().getScrollPane().repaint() |
| | + ) |
| | + ); |
| | + } |
| | + |
| | + return new Scene( borderPane ); |
| | + } |
| | + |
| | + private Text createLineNumberText() { |
| | + return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) ); |
| | + } |
| | + |
| | + private Node createMenuBar() { |
| | + final BooleanBinding activeFileEditorIsNull = |
| | + getFileEditorPane().activeFileEditorProperty().isNull(); |
| | + |
| | + // File actions |
| | + final Action fileNewAction = new ActionBuilder() |
| | + .setText( "Main.menu.file.new" ) |
| | + .setAccelerator( "Shortcut+N" ) |
| | + .setIcon( FILE_ALT ) |
| | + .setAction( e -> fileNew() ) |
| | + .build(); |
| | + final Action fileOpenAction = new ActionBuilder() |
| | + .setText( "Main.menu.file.open" ) |
| | + .setAccelerator( "Shortcut+O" ) |
| | + .setIcon( FOLDER_OPEN_ALT ) |
| | + .setAction( e -> fileOpen() ) |
| | + .build(); |
| | + final Action fileCloseAction = new ActionBuilder() |
| | + .setText( "Main.menu.file.close" ) |
| | + .setAccelerator( "Shortcut+W" ) |
| | + .setAction( e -> fileClose() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action fileCloseAllAction = new ActionBuilder() |
| | + .setText( "Main.menu.file.close_all" ) |
| | + .setAction( e -> fileCloseAll() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action fileSaveAction = new ActionBuilder() |
| | + .setText( "Main.menu.file.save" ) |
| | + .setAccelerator( "Shortcut+S" ) |
| | + .setIcon( FLOPPY_ALT ) |
| | + .setAction( e -> fileSave() ) |
| | + .setDisable( createActiveBooleanProperty( |
| | + FileEditorTab::modifiedProperty ).not() ) |
| | + .build(); |
| | + final Action fileSaveAsAction = new ActionBuilder() |
| | + .setText( "Main.menu.file.save_as" ) |
| | + .setAction( e -> fileSaveAs() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action fileSaveAllAction = new ActionBuilder() |
| | + .setText( "Main.menu.file.save_all" ) |
| | + .setAccelerator( "Shortcut+Shift+S" ) |
| | + .setAction( e -> fileSaveAll() ) |
| | + .setDisable( Bindings.not( |
| | + getFileEditorPane().anyFileEditorModifiedProperty() ) ) |
| | + .build(); |
| | + final Action fileExitAction = new ActionBuilder() |
| | + .setText( "Main.menu.file.exit" ) |
| | + .setAction( e -> fileExit() ) |
| | + .build(); |
| | + |
| | + // Edit actions |
| | + final Action editCopyHtmlAction = new ActionBuilder() |
| | + .setText( Messages.get( "Main.menu.edit.copy.html" ) ) |
| | + .setIcon( HTML5 ) |
| | + .setAction( e -> copyHtml() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + |
| | + final Action editUndoAction = new ActionBuilder() |
| | + .setText( "Main.menu.edit.undo" ) |
| | + .setAccelerator( "Shortcut+Z" ) |
| | + .setIcon( UNDO ) |
| | + .setAction( e -> getActiveEditorPane().undo() ) |
| | + .setDisable( createActiveBooleanProperty( |
| | + FileEditorTab::canUndoProperty ).not() ) |
| | + .build(); |
| | + final Action editRedoAction = new ActionBuilder() |
| | + .setText( "Main.menu.edit.redo" ) |
| | + .setAccelerator( "Shortcut+Y" ) |
| | + .setIcon( REPEAT ) |
| | + .setAction( e -> getActiveEditorPane().redo() ) |
| | + .setDisable( createActiveBooleanProperty( |
| | + FileEditorTab::canRedoProperty ).not() ) |
| | + .build(); |
| | + |
| | + final Action editCutAction = new ActionBuilder() |
| | + .setText( Messages.get( "Main.menu.edit.cut" ) ) |
| | + .setAccelerator( "Shortcut+X" ) |
| | + .setIcon( CUT ) |
| | + .setAction( e -> getActiveEditorPane().cut() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action editCopyAction = new ActionBuilder() |
| | + .setText( Messages.get( "Main.menu.edit.copy" ) ) |
| | + .setAccelerator( "Shortcut+C" ) |
| | + .setIcon( COPY ) |
| | + .setAction( e -> getActiveEditorPane().copy() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action editPasteAction = new ActionBuilder() |
| | + .setText( Messages.get( "Main.menu.edit.paste" ) ) |
| | + .setAccelerator( "Shortcut+V" ) |
| | + .setIcon( PASTE ) |
| | + .setAction( e -> getActiveEditorPane().paste() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action editSelectAllAction = new ActionBuilder() |
| | + .setText( Messages.get( "Main.menu.edit.selectAll" ) ) |
| | + .setAccelerator( "Shortcut+A" ) |
| | + .setAction( e -> getActiveEditorPane().selectAll() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + |
| | + final Action editFindAction = new ActionBuilder() |
| | + .setText( "Main.menu.edit.find" ) |
| | + .setAccelerator( "Ctrl+F" ) |
| | + .setIcon( SEARCH ) |
| | + .setAction( e -> editFind() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action editFindNextAction = new ActionBuilder() |
| | + .setText( "Main.menu.edit.find.next" ) |
| | + .setAccelerator( "F3" ) |
| | + .setIcon( null ) |
| | + .setAction( e -> editFindNext() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action editPreferencesAction = new ActionBuilder() |
| | + .setText( "Main.menu.edit.preferences" ) |
| | + .setAccelerator( "Ctrl+Alt+S" ) |
| | + .setAction( e -> editPreferences() ) |
| | + .build(); |
| | + |
| | + // Insert actions |
| | + final Action insertBoldAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.bold" ) |
| | + .setAccelerator( "Shortcut+B" ) |
| | + .setIcon( BOLD ) |
| | + .setAction( e -> insertMarkdown( "**", "**" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertItalicAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.italic" ) |
| | + .setAccelerator( "Shortcut+I" ) |
| | + .setIcon( ITALIC ) |
| | + .setAction( e -> insertMarkdown( "*", "*" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertSuperscriptAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.superscript" ) |
| | + .setAccelerator( "Shortcut+[" ) |
| | + .setIcon( SUPERSCRIPT ) |
| | + .setAction( e -> insertMarkdown( "^", "^" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertSubscriptAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.subscript" ) |
| | + .setAccelerator( "Shortcut+]" ) |
| | + .setIcon( SUBSCRIPT ) |
| | + .setAction( e -> insertMarkdown( "~", "~" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertStrikethroughAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.strikethrough" ) |
| | + .setAccelerator( "Shortcut+T" ) |
| | + .setIcon( STRIKETHROUGH ) |
| | + .setAction( e -> insertMarkdown( "~~", "~~" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertBlockquoteAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.blockquote" ) |
| | + .setAccelerator( "Ctrl+Q" ) |
| | + .setIcon( QUOTE_LEFT ) |
| | + .setAction( e -> insertMarkdown( "\n\n> ", "" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertCodeAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.code" ) |
| | + .setAccelerator( "Shortcut+K" ) |
| | + .setIcon( CODE ) |
| | + .setAction( e -> insertMarkdown( "`", "`" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertFencedCodeBlockAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.fenced_code_block" ) |
| | + .setAccelerator( "Shortcut+Shift+K" ) |
| | + .setIcon( FILE_CODE_ALT ) |
| | + .setAction( e -> insertMarkdown( |
| | + "\n\n```\n", |
| | + "\n```\n\n", |
| | + get( "Main.menu.insert.fenced_code_block.prompt" ) ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertLinkAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.link" ) |
| | + .setAccelerator( "Shortcut+L" ) |
| | + .setIcon( LINK ) |
| | + .setAction( e -> getActiveEditorPane().insertLink() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertImageAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.image" ) |
| | + .setAccelerator( "Shortcut+G" ) |
| | + .setIcon( PICTURE_ALT ) |
| | + .setAction( e -> getActiveEditorPane().insertImage() ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + |
| | + // Number of heading actions (H1 ... H3) |
| | + final int HEADINGS = 3; |
| | + final Action[] headings = new Action[ HEADINGS ]; |
| | + |
| | + for( int i = 1; i <= HEADINGS; i++ ) { |
| | + final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); |
| | + final String markup = String.format( "%n%n%s ", hashes ); |
| | + final String text = "Main.menu.insert.heading." + i; |
| | + final String accelerator = "Shortcut+" + i; |
| | + final String prompt = text + ".prompt"; |
| | + |
| | + headings[ i - 1 ] = new ActionBuilder() |
| | + .setText( text ) |
| | + .setAccelerator( accelerator ) |
| | + .setIcon( HEADER ) |
| | + .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + } |
| | + |
| | + final Action insertUnorderedListAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.unordered_list" ) |
| | + .setAccelerator( "Shortcut+U" ) |
| | + .setIcon( LIST_UL ) |
| | + .setAction( e -> insertMarkdown( "\n\n* ", "" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertOrderedListAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.ordered_list" ) |
| | + .setAccelerator( "Shortcut+Shift+O" ) |
| | + .setIcon( LIST_OL ) |
| | + .setAction( e -> insertMarkdown( |
| | + "\n\n1. ", "" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + final Action insertHorizontalRuleAction = new ActionBuilder() |
| | + .setText( "Main.menu.insert.horizontal_rule" ) |
| | + .setAccelerator( "Shortcut+H" ) |
| | + .setAction( e -> insertMarkdown( |
| | + "\n\n---\n\n", "" ) ) |
| | + .setDisable( activeFileEditorIsNull ) |
| | + .build(); |
| | + |
| | + // View actions |
| | + final Action viewRefreshAction = new ActionBuilder() |
| | + .setText( "Main.menu.view.refresh" ) |
| | + .setAccelerator( "F5" ) |
| | + .setAction( e -> viewRefresh() ) |
| | + .build(); |
| | + |
| | + // Help actions |
| | + final Action helpAboutAction = new ActionBuilder() |
| | + .setText( "Main.menu.help.about" ) |
| | + .setAction( e -> helpAbout() ) |
| | + .build(); |
| | + |
| | + //---- MenuBar ---- |
| | + final Menu fileMenu = ActionUtils.createMenu( |
| | + get( "Main.menu.file" ), |
| | + fileNewAction, |
| | + fileOpenAction, |
| | + null, |
| | + fileCloseAction, |
| | + fileCloseAllAction, |
| | + null, |
| | + fileSaveAction, |
| | + fileSaveAsAction, |
| | + fileSaveAllAction, |
| | + null, |
| | + fileExitAction ); |
| | + |
| | + final Menu editMenu = ActionUtils.createMenu( |
| | + get( "Main.menu.edit" ), |
| | + editCopyHtmlAction, |
| | + null, |
| | + editUndoAction, |
| | + editRedoAction, |
| | + null, |
| | + editCutAction, |
| | + editCopyAction, |
| | + editPasteAction, |
| | + editSelectAllAction, |
| | + null, |
| | + editFindAction, |
| | + editFindNextAction, |
| | + null, |
| | + editPreferencesAction ); |
| | + |
| | + final Menu insertMenu = ActionUtils.createMenu( |
| | + get( "Main.menu.insert" ), |
| | + insertBoldAction, |
| | + insertItalicAction, |
| | + insertSuperscriptAction, |
| | + insertSubscriptAction, |
| | + insertStrikethroughAction, |
| | + insertBlockquoteAction, |
| | + insertCodeAction, |
| | + insertFencedCodeBlockAction, |
| | + null, |
| | + insertLinkAction, |
| | + insertImageAction, |
| | + null, |
| | + headings[ 0 ], |
| | + headings[ 1 ], |
| | + headings[ 2 ], |
| | + null, |
| | + insertUnorderedListAction, |
| | + insertOrderedListAction, |
| | + insertHorizontalRuleAction |
| | + ); |
| | + |
| | + final Menu viewMenu = ActionUtils.createMenu( |
| | + get( "Main.menu.view" ), |
| | + viewRefreshAction ); |
| | + |
| | + final Menu helpMenu = ActionUtils.createMenu( |
| | + get( "Main.menu.help" ), |
| | + helpAboutAction ); |
| | + |
| | + final MenuBar menuBar = new MenuBar( |
| | + fileMenu, |
| | + editMenu, |
| | + insertMenu, |
| | + viewMenu, |
| | + helpMenu ); |
| | + |
| | + //---- ToolBar ---- |
| | + final ToolBar toolBar = ActionUtils.createToolBar( |
| | + fileNewAction, |
| | + fileOpenAction, |
| | + fileSaveAction, |
| | + null, |
| | + editUndoAction, |
| | + editRedoAction, |
| | + editCutAction, |
| | + editCopyAction, |
| | + editPasteAction, |
| | + null, |
| | + insertBoldAction, |
| | + insertItalicAction, |
| | + insertSuperscriptAction, |
| | + insertSubscriptAction, |
| | + insertBlockquoteAction, |
| | + insertCodeAction, |
| | + insertFencedCodeBlockAction, |
| | + null, |
| | + insertLinkAction, |
| | + insertImageAction, |
| | + null, |
| | + headings[ 0 ], |
| | + null, |
| | + insertUnorderedListAction, |
| | + insertOrderedListAction ); |
| | + |
| | + return new VBox( menuBar, toolBar ); |
| | + } |
| | + |
| | + /** |
| | + * Creates a boolean property that is bound to another boolean value of the |
| | + * active editor. |
| | + */ |
| | + private BooleanProperty createActiveBooleanProperty( |
| | + final Function<FileEditorTab, ObservableBooleanValue> func ) { |
| | + |
| | + final BooleanProperty b = new SimpleBooleanProperty(); |
| | + final FileEditorTab tab = getActiveFileEditorTab(); |
| | + |
| | + if( tab != null ) { |
| | + b.bind( func.apply( tab ) ); |
| | + } |
| | + |
| | + getFileEditorPane().activeFileEditorProperty().addListener( |
| | + ( observable, oldFileEditor, newFileEditor ) -> { |
| | + b.unbind(); |
| | + |
| | + if( newFileEditor == null ) { |
| | + b.set( false ); |
| | + } |
| | + else { |
| | + b.bind( func.apply( newFileEditor ) ); |
| | + } |
| | + } |
| | + ); |
| | + |
| | + return b; |
| | + } |
| | + |
| | + //---- Convenience accessors ---------------------------------------------- |
| | + |
| | + private Preferences getPreferences() { |
| | + return sOptions.getState(); |
| | + } |
| | + |
| | + private int getCurrentParagraphIndex() { |
| | + return getActiveEditorPane().getCurrentParagraphIndex(); |
| | + } |
| | + |
| | + private float getFloat( final String key, final float defaultValue ) { |
| | + return getPreferences().getFloat( key, defaultValue ); |
| | + } |
| | + |
| | + public Window getWindow() { |
| | + return getScene().getWindow(); |
| | + } |
| | + |
| | + private MarkdownEditorPane getActiveEditorPane() { |
| | + return getActiveFileEditorTab().getEditorPane(); |
| | + } |
| | + |
| | + private FileEditorTab getActiveFileEditorTab() { |
| | + return getFileEditorPane().getActiveFileEditor(); |
| | + } |
| | + |
| | + //---- Member accessors --------------------------------------------------- |
| | + |
| | + protected Scene getScene() { |
| | + return mScene; |
| | + } |
| | + |
| | + private SpellChecker getSpellChecker() { |
| | + return mSpellChecker; |
| | + } |
| | + |
| | + private Map<FileEditorTab, Processor<String>> getProcessors() { |
| | + return mProcessors; |
| | + } |
| | + |
| | + private FileEditorTabPane getFileEditorPane() { |
| | + return mFileEditorPane; |
| | + } |
| | + |
| | + private HTMLPreviewPane getPreviewPane() { |
| | + return mPreviewPane; |
| | + } |
| | + |
| | + private void setDefinitionSource( |
| | + final DefinitionSource definitionSource ) { |
| | + assert definitionSource != null; |
| | + mDefinitionSource = definitionSource; |
| | + } |
| | + |
| | + private DefinitionSource getDefinitionSource() { |
| | + return mDefinitionSource; |
| | + } |
| | + |
| | + private DefinitionPane getDefinitionPane() { |
| | + return mDefinitionPane; |
| | + } |
| | + |
| | + private Text getLineNumberText() { |
| | + return mLineNumberText; |
| | + } |
| | + |
| | + private StatusBar getStatusBar() { |
| | + return mStatusBar; |
| | + } |
| | + |
| | + private TextField getFindTextField() { |
| | + return mFindTextField; |
| | + } |
| | + |
| | + private DefinitionNameInjector getVariableNameInjector() { |
| | + return mVariableNameInjector; |
| | + } |
| | + |
| | + /** |
| | + * Returns the variable map of interpolated definitions. |
| | + * |
| | + * @return A map to help dereference variables. |
| | + */ |
| | + private Map<String, String> getResolvedMap() { |
| | + return mResolvedMap; |
| | + } |
| | + |
| | + private static Notifier getNotifier() { |
| | return sNotifier; |
| | } |