| | addEventListener( keyPressed( TAB ), this::tab ); |
| | addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab ); |
| | - //addEventListener( keyPressed( INSERT ), this::onInsertPressed ); |
| | - addEventListener( keyPressed( ENTER, ALT_DOWN ), this::autofix ); |
| | - } |
| | - |
| | - private void initUndoManager() { |
| | - final var undoManager = getUndoManager(); |
| | - final var markedPosition = undoManager.atMarkedPositionProperty(); |
| | - |
| | - undoManager.forgetHistory(); |
| | - undoManager.mark(); |
| | - mModified.bind( Bindings.not( markedPosition ) ); |
| | - } |
| | - |
| | - @Override |
| | - public void moveTo( final int offset ) { |
| | - assert 0 <= offset && offset <= mTextArea.getLength(); |
| | - |
| | - mTextArea.moveTo( offset ); |
| | - mTextArea.requestFollowCaret(); |
| | - } |
| | - |
| | - /** |
| | - * Delegate the focus request to the text area itself. |
| | - */ |
| | - @Override |
| | - public void requestFocus() { |
| | - mTextArea.requestFocus(); |
| | - } |
| | - |
| | - @Override |
| | - public void setText( final String text ) { |
| | - mTextArea.clear(); |
| | - mTextArea.appendText( text ); |
| | - mTextArea.getUndoManager().mark(); |
| | - } |
| | - |
| | - @Override |
| | - public String getText() { |
| | - return mTextArea.getText(); |
| | - } |
| | - |
| | - @Override |
| | - public Charset getEncoding() { |
| | - return mEncoding; |
| | - } |
| | - |
| | - @Override |
| | - public File getFile() { |
| | - return mFile; |
| | - } |
| | - |
| | - @Override |
| | - public void rename( final File file ) { |
| | - mFile = file; |
| | - } |
| | - |
| | - @Override |
| | - public void undo() { |
| | - final var manager = getUndoManager(); |
| | - xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" ); |
| | - } |
| | - |
| | - @Override |
| | - public void redo() { |
| | - final var manager = getUndoManager(); |
| | - xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" ); |
| | - } |
| | - |
| | - /** |
| | - * Performs an undo or redo action, if possible, otherwise displays an error |
| | - * message to the user. |
| | - * |
| | - * @param ready Answers whether the action can be executed. |
| | - * @param action The action to execute. |
| | - * @param key The informational message key having a value to display if |
| | - * the {@link Supplier} is not ready. |
| | - */ |
| | - private void xxdo( |
| | - final Supplier<Boolean> ready, final Runnable action, final String key ) { |
| | - if( ready.get() ) { |
| | - action.run(); |
| | - } |
| | - else { |
| | - clue( key ); |
| | - } |
| | - } |
| | - |
| | - @Override |
| | - public void cut() { |
| | - final var selected = mTextArea.getSelectedText(); |
| | - |
| | - // Emulate selecting the current line by firing Home then Shift+Down Arrow. |
| | - if( selected == null || selected.isEmpty() ) { |
| | - // Note: mTextArea.selectLine() does not select empty lines. |
| | - mTextArea.fireEvent( keyDown( HOME, false ) ); |
| | - mTextArea.fireEvent( keyDown( DOWN, true ) ); |
| | - } |
| | - |
| | - mTextArea.cut(); |
| | - } |
| | - |
| | - @Override |
| | - public void copy() { |
| | - mTextArea.copy(); |
| | - } |
| | - |
| | - @Override |
| | - public void paste() { |
| | - mTextArea.paste(); |
| | - } |
| | - |
| | - @Override |
| | - public void selectAll() { |
| | - mTextArea.selectAll(); |
| | - } |
| | - |
| | - @Override |
| | - public void bold() { |
| | - enwrap( "**" ); |
| | - } |
| | - |
| | - @Override |
| | - public void italic() { |
| | - enwrap( "*" ); |
| | - } |
| | - |
| | - @Override |
| | - public void monospace() { |
| | - enwrap( "`" ); |
| | - } |
| | - |
| | - @Override |
| | - public void superscript() { |
| | - enwrap( "^" ); |
| | - } |
| | - |
| | - @Override |
| | - public void subscript() { |
| | - enwrap( "~" ); |
| | - } |
| | - |
| | - @Override |
| | - public void strikethrough() { |
| | - enwrap( "~~" ); |
| | - } |
| | - |
| | - @Override |
| | - public void blockquote() { |
| | - block( "> " ); |
| | - } |
| | - |
| | - @Override |
| | - public void code() { |
| | - enwrap( "`" ); |
| | - } |
| | - |
| | - @Override |
| | - public void fencedCodeBlock() { |
| | - enwrap( "\n\n```\n", "\n```\n\n" ); |
| | - } |
| | - |
| | - @Override |
| | - public void heading( final int level ) { |
| | - final var hashes = new String( new char[ level ] ).replace( "\0", "#" ); |
| | - block( format( "%s ", hashes ) ); |
| | - } |
| | - |
| | - @Override |
| | - public void unorderedList() { |
| | - block( "* " ); |
| | - } |
| | - |
| | - @Override |
| | - public void orderedList() { |
| | - block( "1. " ); |
| | - } |
| | - |
| | - @Override |
| | - public void horizontalRule() { |
| | - block( format( "---%n%n" ) ); |
| | - } |
| | - |
| | - @Override |
| | - public Node getNode() { |
| | - return this; |
| | - } |
| | - |
| | - @Override |
| | - public ReadOnlyBooleanProperty modifiedProperty() { |
| | - return mModified; |
| | - } |
| | - |
| | - @Override |
| | - public void clearModifiedProperty() { |
| | - getUndoManager().mark(); |
| | - } |
| | - |
| | - @Override |
| | - public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { |
| | - return mScrollPane; |
| | - } |
| | - |
| | - @Override |
| | - public StyleClassedTextArea getTextArea() { |
| | - return mTextArea; |
| | - } |
| | - |
| | - private final Map<String, IndexRange> mStyles = new HashMap<>(); |
| | - |
| | - @Override |
| | - public void stylize( final IndexRange range, final String style ) { |
| | - final var began = range.getStart(); |
| | - final var ended = range.getEnd() + 1; |
| | - |
| | - assert 0 <= began && began <= ended; |
| | - assert style != null; |
| | - |
| | - // TODO: Ensure spell check and find highlights can coexist. |
| | -// final var spans = mTextArea.getStyleSpans( range ); |
| | -// System.out.println( "SPANS: " + spans ); |
| | - |
| | -// final var spans = mTextArea.getStyleSpans( range ); |
| | -// mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style |
| | -// ) ); |
| | - |
| | -// final var builder = new StyleSpansBuilder<Collection<String>>(); |
| | -// builder.add( singleton( style ), range.getLength() + 1 ); |
| | -// mTextArea.setStyleSpans( began, builder.create() ); |
| | - |
| | -// final var s = mTextArea.getStyleSpans( began, ended ); |
| | -// System.out.println( "STYLES: " +s ); |
| | - |
| | - mStyles.put( style, range ); |
| | - mTextArea.setStyleClass( began, ended, style ); |
| | - |
| | - // Ensure that whenever the user interacts with the text that the found |
| | - // word will have its highlighting removed. The handler removes itself. |
| | - // This won't remove the highlighting if the caret position moves by mouse. |
| | - final var handler = mTextArea.getOnKeyPressed(); |
| | - mTextArea.setOnKeyPressed( ( event ) -> { |
| | - mTextArea.setOnKeyPressed( handler ); |
| | - unstylize( style ); |
| | - } ); |
| | - |
| | - //mTextArea.setStyleSpans(began, ended, s); |
| | - } |
| | - |
| | - private static StyleSpans<Collection<String>> merge( |
| | - StyleSpans<Collection<String>> spans, int len, String style ) { |
| | - spans = spans.overlay( |
| | - singleton( singletonList( style ), len ), |
| | - ( bottomSpan, list ) -> { |
| | - final List<String> l = |
| | - new ArrayList<>( bottomSpan.size() + list.size() ); |
| | - l.addAll( bottomSpan ); |
| | - l.addAll( list ); |
| | - return l; |
| | - } ); |
| | - |
| | - return spans; |
| | - } |
| | - |
| | - @Override |
| | - public void unstylize( final String style ) { |
| | - final var indexes = mStyles.remove( style ); |
| | - if( indexes != null ) { |
| | - mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 ); |
| | - } |
| | - } |
| | - |
| | - @Override |
| | - public Caret getCaret() { |
| | - return mCaret; |
| | - } |
| | - |
| | - private Caret createCaret( final StyleClassedTextArea editor ) { |
| | - return Caret |
| | - .builder() |
| | - .with( Caret.Mutator::setEditor, editor ) |
| | - .build(); |
| | - } |
| | - |
| | - /** |
| | - * This method adds listeners to editor events. |
| | - * |
| | - * @param <T> The event type. |
| | - * @param <U> The consumer type for the given event type. |
| | - * @param event The event of interest. |
| | - * @param consumer The method to call when the event happens. |
| | - */ |
| | - public <T extends Event, U extends T> void addEventListener( |
| | - final EventPattern<? super T, ? extends U> event, |
| | - final Consumer<? super U> consumer ) { |
| | - Nodes.addInputMap( mTextArea, consume( event, consumer ) ); |
| | - } |
| | - |
| | - private void onEnterPressed( final KeyEvent ignored ) { |
| | - final var currentLine = getCaretParagraph(); |
| | - final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine ); |
| | - |
| | - // By default, insert a new line by itself. |
| | - String newText = NEWLINE; |
| | - |
| | - // If the pattern was matched then determine what block type to continue. |
| | - if( matcher.matches() ) { |
| | - if( matcher.group( 2 ).isEmpty() ) { |
| | - final var pos = mTextArea.getCaretPosition(); |
| | - mTextArea.selectRange( pos - currentLine.length(), pos ); |
| | - } |
| | - else { |
| | - // Indent the new line with the same whitespace characters and |
| | - // list markers as current line. This ensures that the indentation |
| | - // is propagated. |
| | - newText = newText.concat( matcher.group( 1 ) ); |
| | - } |
| | - } |
| | - |
| | - mTextArea.replaceSelection( newText ); |
| | - } |
| | - |
| | - /** |
| | - * TODO: 105 - Insert key toggle overwrite (typeover) mode |
| | - * |
| | - * @param ignored Unused. |
| | - */ |
| | - private void onInsertPressed( final KeyEvent ignored ) { |
| | + addEventListener( keyPressed( ENTER, ALT_DOWN ), this::autofix ); |
| | + } |
| | + |
| | + private void initUndoManager() { |
| | + final var undoManager = getUndoManager(); |
| | + final var markedPosition = undoManager.atMarkedPositionProperty(); |
| | + |
| | + undoManager.forgetHistory(); |
| | + undoManager.mark(); |
| | + mModified.bind( Bindings.not( markedPosition ) ); |
| | + } |
| | + |
| | + @Override |
| | + public void moveTo( final int offset ) { |
| | + assert 0 <= offset && offset <= mTextArea.getLength(); |
| | + |
| | + mTextArea.moveTo( offset ); |
| | + mTextArea.requestFollowCaret(); |
| | + } |
| | + |
| | + /** |
| | + * Delegate the focus request to the text area itself. |
| | + */ |
| | + @Override |
| | + public void requestFocus() { |
| | + mTextArea.requestFocus(); |
| | + } |
| | + |
| | + @Override |
| | + public void setText( final String text ) { |
| | + mTextArea.clear(); |
| | + mTextArea.appendText( text ); |
| | + mTextArea.getUndoManager().mark(); |
| | + } |
| | + |
| | + @Override |
| | + public String getText() { |
| | + return mTextArea.getText(); |
| | + } |
| | + |
| | + @Override |
| | + public Charset getEncoding() { |
| | + return mEncoding; |
| | + } |
| | + |
| | + @Override |
| | + public File getFile() { |
| | + return mFile; |
| | + } |
| | + |
| | + @Override |
| | + public void rename( final File file ) { |
| | + mFile = file; |
| | + } |
| | + |
| | + @Override |
| | + public void undo() { |
| | + final var manager = getUndoManager(); |
| | + xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" ); |
| | + } |
| | + |
| | + @Override |
| | + public void redo() { |
| | + final var manager = getUndoManager(); |
| | + xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" ); |
| | + } |
| | + |
| | + /** |
| | + * Performs an undo or redo action, if possible, otherwise displays an error |
| | + * message to the user. |
| | + * |
| | + * @param ready Answers whether the action can be executed. |
| | + * @param action The action to execute. |
| | + * @param key The informational message key having a value to display if |
| | + * the {@link Supplier} is not ready. |
| | + */ |
| | + private void xxdo( |
| | + final Supplier<Boolean> ready, final Runnable action, final String key ) { |
| | + if( ready.get() ) { |
| | + action.run(); |
| | + } |
| | + else { |
| | + clue( key ); |
| | + } |
| | + } |
| | + |
| | + @Override |
| | + public void cut() { |
| | + final var selected = mTextArea.getSelectedText(); |
| | + |
| | + // Emulate selecting the current line by firing Home then Shift+Down Arrow. |
| | + if( selected == null || selected.isEmpty() ) { |
| | + // Note: mTextArea.selectLine() does not select empty lines. |
| | + mTextArea.fireEvent( keyDown( HOME, false ) ); |
| | + mTextArea.fireEvent( keyDown( DOWN, true ) ); |
| | + } |
| | + |
| | + mTextArea.cut(); |
| | + } |
| | + |
| | + @Override |
| | + public void copy() { |
| | + mTextArea.copy(); |
| | + } |
| | + |
| | + @Override |
| | + public void paste() { |
| | + mTextArea.paste(); |
| | + } |
| | + |
| | + @Override |
| | + public void selectAll() { |
| | + mTextArea.selectAll(); |
| | + } |
| | + |
| | + @Override |
| | + public void bold() { |
| | + enwrap( "**" ); |
| | + } |
| | + |
| | + @Override |
| | + public void italic() { |
| | + enwrap( "*" ); |
| | + } |
| | + |
| | + @Override |
| | + public void monospace() { |
| | + enwrap( "`" ); |
| | + } |
| | + |
| | + @Override |
| | + public void superscript() { |
| | + enwrap( "^" ); |
| | + } |
| | + |
| | + @Override |
| | + public void subscript() { |
| | + enwrap( "~" ); |
| | + } |
| | + |
| | + @Override |
| | + public void strikethrough() { |
| | + enwrap( "~~" ); |
| | + } |
| | + |
| | + @Override |
| | + public void blockquote() { |
| | + block( "> " ); |
| | + } |
| | + |
| | + @Override |
| | + public void code() { |
| | + enwrap( "`" ); |
| | + } |
| | + |
| | + @Override |
| | + public void fencedCodeBlock() { |
| | + enwrap( "\n\n```\n", "\n```\n\n" ); |
| | + } |
| | + |
| | + @Override |
| | + public void heading( final int level ) { |
| | + final var hashes = new String( new char[ level ] ).replace( "\0", "#" ); |
| | + block( format( "%s ", hashes ) ); |
| | + } |
| | + |
| | + @Override |
| | + public void unorderedList() { |
| | + block( "* " ); |
| | + } |
| | + |
| | + @Override |
| | + public void orderedList() { |
| | + block( "1. " ); |
| | + } |
| | + |
| | + @Override |
| | + public void horizontalRule() { |
| | + block( format( "---%n%n" ) ); |
| | + } |
| | + |
| | + @Override |
| | + public Node getNode() { |
| | + return this; |
| | + } |
| | + |
| | + @Override |
| | + public ReadOnlyBooleanProperty modifiedProperty() { |
| | + return mModified; |
| | + } |
| | + |
| | + @Override |
| | + public void clearModifiedProperty() { |
| | + getUndoManager().mark(); |
| | + } |
| | + |
| | + @Override |
| | + public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { |
| | + return mScrollPane; |
| | + } |
| | + |
| | + @Override |
| | + public StyleClassedTextArea getTextArea() { |
| | + return mTextArea; |
| | + } |
| | + |
| | + private final Map<String, IndexRange> mStyles = new HashMap<>(); |
| | + |
| | + @Override |
| | + public void stylize( final IndexRange range, final String style ) { |
| | + final var began = range.getStart(); |
| | + final var ended = range.getEnd() + 1; |
| | + |
| | + assert 0 <= began && began <= ended; |
| | + assert style != null; |
| | + |
| | + // TODO: Ensure spell check and find highlights can coexist. |
| | +// final var spans = mTextArea.getStyleSpans( range ); |
| | +// System.out.println( "SPANS: " + spans ); |
| | + |
| | +// final var spans = mTextArea.getStyleSpans( range ); |
| | +// mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style |
| | +// ) ); |
| | + |
| | +// final var builder = new StyleSpansBuilder<Collection<String>>(); |
| | +// builder.add( singleton( style ), range.getLength() + 1 ); |
| | +// mTextArea.setStyleSpans( began, builder.create() ); |
| | + |
| | +// final var s = mTextArea.getStyleSpans( began, ended ); |
| | +// System.out.println( "STYLES: " +s ); |
| | + |
| | + mStyles.put( style, range ); |
| | + mTextArea.setStyleClass( began, ended, style ); |
| | + |
| | + // Ensure that whenever the user interacts with the text that the found |
| | + // word will have its highlighting removed. The handler removes itself. |
| | + // This won't remove the highlighting if the caret position moves by mouse. |
| | + final var handler = mTextArea.getOnKeyPressed(); |
| | + mTextArea.setOnKeyPressed( ( event ) -> { |
| | + mTextArea.setOnKeyPressed( handler ); |
| | + unstylize( style ); |
| | + } ); |
| | + |
| | + //mTextArea.setStyleSpans(began, ended, s); |
| | + } |
| | + |
| | + private static StyleSpans<Collection<String>> merge( |
| | + StyleSpans<Collection<String>> spans, int len, String style ) { |
| | + spans = spans.overlay( |
| | + singleton( singletonList( style ), len ), |
| | + ( bottomSpan, list ) -> { |
| | + final List<String> l = |
| | + new ArrayList<>( bottomSpan.size() + list.size() ); |
| | + l.addAll( bottomSpan ); |
| | + l.addAll( list ); |
| | + return l; |
| | + } ); |
| | + |
| | + return spans; |
| | + } |
| | + |
| | + @Override |
| | + public void unstylize( final String style ) { |
| | + final var indexes = mStyles.remove( style ); |
| | + if( indexes != null ) { |
| | + mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 ); |
| | + } |
| | + } |
| | + |
| | + @Override |
| | + public Caret getCaret() { |
| | + return mCaret; |
| | + } |
| | + |
| | + private Caret createCaret( final StyleClassedTextArea editor ) { |
| | + return Caret |
| | + .builder() |
| | + .with( Caret.Mutator::setEditor, editor ) |
| | + .build(); |
| | + } |
| | + |
| | + /** |
| | + * This method adds listeners to editor events. |
| | + * |
| | + * @param <T> The event type. |
| | + * @param <U> The consumer type for the given event type. |
| | + * @param event The event of interest. |
| | + * @param consumer The method to call when the event happens. |
| | + */ |
| | + public <T extends Event, U extends T> void addEventListener( |
| | + final EventPattern<? super T, ? extends U> event, |
| | + final Consumer<? super U> consumer ) { |
| | + Nodes.addInputMap( mTextArea, consume( event, consumer ) ); |
| | + } |
| | + |
| | + private void onEnterPressed( final KeyEvent ignored ) { |
| | + final var currentLine = getCaretParagraph(); |
| | + final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine ); |
| | + |
| | + // By default, insert a new line by itself. |
| | + String newText = NEWLINE; |
| | + |
| | + // If the pattern was matched then determine what block type to continue. |
| | + if( matcher.matches() ) { |
| | + if( matcher.group( 2 ).isEmpty() ) { |
| | + final var pos = mTextArea.getCaretPosition(); |
| | + mTextArea.selectRange( pos - currentLine.length(), pos ); |
| | + } |
| | + else { |
| | + // Indent the new line with the same whitespace characters and |
| | + // list markers as current line. This ensures that the indentation |
| | + // is propagated. |
| | + newText = newText.concat( matcher.group( 1 ) ); |
| | + } |
| | + } |
| | + |
| | + mTextArea.replaceSelection( newText ); |
| | } |
| | |