Dave Jarvis' Repositories

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

Simplifying variable editor code.

Authordjarvis <email>
Date2016-11-05 22:19:16 GMT-0700
Commit2a5824eadfb85f279ed6d1f282bfaf2e07268460
Parent9800e15
src/main/java/com/scrivendor/FileEditorPane.java
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
-import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.StyledTextArea;
-import org.fxmisc.richtext.model.TextEditingArea;
import org.fxmisc.wellbehaved.event.EventPattern;
import org.fxmisc.wellbehaved.event.InputMap;
// update activeFileEditor property
tabPane.getSelectionModel().selectedItemProperty().addListener( (observable, oldTab, newTab) -> {
- activeFileEditor.set( (newTab != null) ? (FileEditor)newTab.getUserData() : null );
+ this.activeFileEditor.set( (newTab != null) ? (FileEditor)newTab.getUserData() : null );
} );
}
}
- anyFileEditorModified.set( modified );
+ this.anyFileEditorModified.set( modified );
};
Node getNode() {
- return tabPane;
+ return this.tabPane;
}
FileEditor getActiveFileEditor() {
- return activeFileEditor.get();
+ return this.activeFileEditor.get();
}
ReadOnlyObjectProperty<FileEditor> activeFileEditorProperty() {
- return activeFileEditor.getReadOnlyProperty();
+ return this.activeFileEditor.getReadOnlyProperty();
}
ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
- return anyFileEditorModified.getReadOnlyProperty();
+ return this.anyFileEditorModified.getReadOnlyProperty();
}
src/main/java/com/scrivendor/MainWindow.java
import com.scrivendor.definition.DefinitionPane;
import com.scrivendor.editor.MarkdownEditorPane;
-import com.scrivendor.options.OptionsDialog;
-import com.scrivendor.util.Action;
-import com.scrivendor.util.ActionUtils;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.event.Event;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Alert.AlertType;
-import javafx.scene.control.IndexRange;
-import javafx.scene.control.Menu;
-import javafx.scene.control.MenuBar;
-import javafx.scene.control.SplitPane;
-import javafx.scene.control.ToolBar;
-import javafx.scene.control.TreeItem;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import javafx.scene.input.InputEvent;
-import javafx.scene.input.KeyCode;
-import static javafx.scene.input.KeyCode.BACK_SPACE;
-import static javafx.scene.input.KeyCode.DIGIT2;
-import static javafx.scene.input.KeyCode.ESCAPE;
-import static javafx.scene.input.KeyCode.MINUS;
-import static javafx.scene.input.KeyCode.PERIOD;
-import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
-import javafx.scene.input.KeyEvent;
-import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
-import static javafx.scene.input.KeyEvent.KEY_PRESSED;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-import org.fxmisc.richtext.StyledTextArea;
-import org.fxmisc.wellbehaved.event.EventPattern;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import org.fxmisc.wellbehaved.event.InputMap;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static org.fxmisc.wellbehaved.event.InputMap.sequence;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- *
- * @author Karl Tauber and White Magic Software, Ltd.
- */
-public class MainWindow {
-
- private Scene scene;
- private FileEditorPane fileEditorPane;
- private DefinitionPane definitionPane;
-
- private MenuBar menuBar;
-
- /**
- * Used to capture keyboard events once the user presses @.
- */
- private InputMap<InputEvent> keyboardMap;
-
- /**
- * Position of the variable in the text when in variable mode.
- */
- private int vModePosition = 0;
-
- public MainWindow() {
- initLayout();
- initKeyboardEventListeners();
- }
-
- private void initLayout() {
- final SplitPane splitPane = new SplitPane(
- getDefinitionPane().getNode(),
- getFileEditorPane().getNode() );
- splitPane.setDividerPositions( .05f, .95f );
-
- BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1024, 800 );
- borderPane.setTop( createMenuBar() );
- borderPane.setCenter( splitPane );
-
- setScene( new Scene( borderPane ) );
- getScene().getStylesheets().add( Constants.STYLESHEET_PREVIEW );
- getScene().windowProperty().addListener(
- (observable, oldWindow, newWindow) -> {
- newWindow.setOnCloseRequest( e -> {
- if( !getFileEditorPane().closeAllEditors() ) {
- e.consume();
- }
- } );
-
- // Workaround JavaFX bug: deselect menubar if window loses focus.
- newWindow.focusedProperty().addListener(
- (obs, oldFocused, newFocused) -> {
- if( !newFocused ) {
- // Send an ESC key event to the menubar
- this.menuBar.fireEvent(
- new KeyEvent(
- KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
- false, false, false, false ) );
- }
- } );
- } );
- }
-
- /**
- * Trap the AT key for inserting YAML variables.
- */
- private void initKeyboardEventListeners() {
- addEventListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::atPressed );
- }
-
- /**
- * The @ symbol is a short-cut to inserting a YAML variable reference.
- *
- * @param e Superfluous information about the key that was pressed.
- */
- private void atPressed( KeyEvent e ) {
- startVMode();
-
- getEditor().deselect();
- setVModePosition();
- }
-
- /**
- * The Esc key is used to exit (escape from) variable mode.
- *
- * @param e Superfluous information about the key that was pressed.
- */
- private void escPressed( KeyEvent e ) {
- e.consume();
- }
-
- /**
- * Ignore typed keys.
- *
- * @param e The key that was typed.
- */
- private void vModeKeyTyped( KeyEvent e ) {
- e.consume();
- }
-
- /**
- * Receives key presses until the user completes the variable selection. This
- * allows the arrow keys to be used for selecting variables.
- *
- * @param e The key that was pressed.
- */
- private void vModeKeyPressed( KeyEvent e ) {
- final KeyCode keyCode = e.getCode();
- boolean parse = false;
-
- switch( keyCode ) {
- case BACK_SPACE:
- keyBackspace();
-
- // Break out of variable mode by back spacing to the original position.
- if( getCaretColumn() > getVModePosition() ) {
- break;
- }
-
- case ESCAPE:
- stopVMode();
- break;
-
- case ENTER:
- case END:
- acceptSelection();
- break;
-
- default:
- if( isVariableNameKey( e ) ) {
- insertText( e.getText() );
- parse = true;
- }
-
- break;
- }
-
- if( parse ) {
- parse();
- }
-
- e.consume();
- }
-
- /**
- * Returns true iff the key code the user typed can be used as part of a YAML
- * variable name.
- *
- * @param keyCode
- *
- * @return
- */
- private boolean isVariableNameKey( KeyEvent event ) {
- final KeyCode keyCode = event.getCode();
-
- return keyCode.isLetterKey()
- || keyCode.isDigitKey()
- || keyCode == PERIOD
- || (event.isShiftDown() && keyCode == MINUS);
- }
-
- /**
- * Called when the user presses the Backspace key.
- */
- private void keyBackspace() {
- final StyledTextArea textArea = getEditor();
- textArea.replaceSelection( "" );
- textArea.deletePreviousChar();
- }
-
- /**
- * Called when the user presses either End or Enter key.
- */
- private void acceptSelection() {
- final StyledTextArea textArea = getEditor();
- final IndexRange range = textArea.getSelection();
-
- if( range != null ) {
- textArea.deselect();
- textArea.moveTo( range.getEnd() );
- }
- }
-
- /**
- * Returns the full text for the paragraph that contains the caret.
- *
- * @return
- */
- private String getCurrentParagraph() {
- final StyledTextArea textArea = getEditor();
- final int paragraph = textArea.getCurrentParagraph();
- return textArea.getText( paragraph );
- }
-
- /**
- * Returns the caret's offset into the current paragraph.
- *
- * @return
- */
- private int getCaretColumn() {
- return getEditor().getCaretColumn();
- }
-
- /**
- * Determines the variable selected by the user.
- */
- private void parse() {
- final String p = getCurrentParagraph();
- final String w = p.substring( getVModePosition(), getCaretColumn() ).trim();
- final DefinitionPane pane = getDefinitionPane();
- final TreeItem<String> node = pane.findNode( w );
- final String lastWord = pane.findLastWord( w );
-
- TreeItem<String> lastNode = node;
-
- for( final TreeItem<String> leaf : node.getChildren() ) {
- if( pane.matches( leaf.getValue(), lastWord ) ) {
- lastNode = leaf;
- break;
- }
- }
-
- pane.collapse();
- pane.expand( node );
-
- String typeAhead = "";
-
- if( !lastNode.isLeaf() ) {
- lastNode.setExpanded( true );
-
- final String nodeValue = lastNode.getValue();
- final int index = nodeValue.indexOf( lastWord );
-
- if( index == 0 && !nodeValue.equals( lastWord ) ) {
- typeAhead = nodeValue.substring( lastWord.length() );
- }
- }
-
- insertText( typeAhead );
-
- System.out.println( "lastNode = " + lastNode.getValue() );
- System.out.println( "lastWord = " + lastWord );
- }
-
- /**
- * Used to lazily initialize the keyboard map.
- *
- * @return Mappings for keyTyped and keyPressed.
- */
- protected InputMap<InputEvent> createKeyboardMap() {
- return sequence(
- consume( keyTyped(), this::vModeKeyTyped ),
- consume( keyPressed(), this::vModeKeyPressed )
- );
- }
-
- private InputMap<InputEvent> getKeyboardMap() {
- if( this.keyboardMap == null ) {
- this.keyboardMap = createKeyboardMap();
- }
-
- return this.keyboardMap;
- }
-
- private void startVMode() {
- addEventListener( getKeyboardMap() );
- }
-
- private void stopVMode() {
- removeEventListener( getKeyboardMap() );
- }
-
- /**
- * Inserts the string at the current caret position.
- *
- * @param s The text to insert.
- */
- private void insertText( final String s ) {
- final StyledTextArea t = getEditor();
-
- final int length = s.length();
- final int posBegan = t.getCaretPosition();
- final int posEnded = posBegan + length;
-
- t.replaceSelection( s );
-
- if( posEnded - posBegan > 1 ) {
- t.selectRange( posEnded, posBegan );
- }
- }
-
- private StyledTextArea getEditor() {
- return getFileEditorPane().getEditor();
- }
-
- private Window getWindow() {
- return getScene().getWindow();
- }
-
- public Scene getScene() {
- return this.scene;
- }
-
- private void setScene( Scene scene ) {
- this.scene = scene;
- }
-
- private Node createMenuBar() {
- BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
-
- // File actions
- Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
- Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
- Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
- Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
- Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
- createActiveBooleanProperty( FileEditor::modifiedProperty ).not() );
- Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
- Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
- Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
-
- // Edit actions
- Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
- e -> getActiveEditor().undo(),
- createActiveBooleanProperty( FileEditor::canUndoProperty ).not() );
- Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
- e -> getActiveEditor().redo(),
- createActiveBooleanProperty( FileEditor::canRedoProperty ).not() );
-
- // Insert actions
- Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
- e -> getActiveEditor().surroundSelection( "**", "**" ),
- activeFileEditorIsNull );
- Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
- e -> getActiveEditor().surroundSelection( "*", "*" ),
- activeFileEditorIsNull );
- Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
- e -> getActiveEditor().surroundSelection( "~~", "~~" ),
- activeFileEditorIsNull );
- Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
- e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
- activeFileEditorIsNull );
- Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
- e -> getActiveEditor().surroundSelection( "`", "`" ),
- activeFileEditorIsNull );
- Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
- e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ),
- activeFileEditorIsNull );
-
- Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
- e -> getActiveEditor().insertLink(),
- activeFileEditorIsNull );
- Action insertImageAction = new Action( Messages.get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
- e -> getActiveEditor().insertImage(),
- activeFileEditorIsNull );
-
- final Action[] headers = new Action[ 6 ];
-
- for( int i = 1; i <= 6; i++ ) {
- final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
- final String markup = String.format( "\n\n%s ", hashes );
- final String text = Messages.get( "Main.menu.insert.header_" + i );
- final String accelerator = "Shortcut+" + i;
- final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" );
-
- headers[ i - 1 ] = new Action( text, accelerator, HEADER,
- e -> getActiveEditor().surroundSelection( markup, "", prompt ),
- activeFileEditorIsNull );
- }
-
- Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
- e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
- activeFileEditorIsNull );
- Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
- e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
- activeFileEditorIsNull );
- Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
- e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
- activeFileEditorIsNull );
-
- // Tools actions
- Action toolsOptionsAction = new Action( Messages.get( "Main.menu.tools.options" ), "Shortcut+,", null, e -> toolsOptions() );
-
- // Help actions
- Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
-
- //---- MenuBar ----
- Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ),
- fileNewAction,
- fileOpenAction,
- null,
- fileCloseAction,
- fileCloseAllAction,
- null,
- fileSaveAction,
- fileSaveAllAction,
- null,
- fileExitAction );
-
- Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ),
- editUndoAction,
- editRedoAction );
-
- Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ),
- insertBoldAction,
- insertItalicAction,
- insertStrikethroughAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headers[ 0 ],
- headers[ 1 ],
- headers[ 2 ],
- headers[ 3 ],
- headers[ 4 ],
- headers[ 5 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction,
- insertHorizontalRuleAction );
-
- Menu toolsMenu = ActionUtils.createMenu( Messages.get( "Main.menu.tools" ),
- toolsOptionsAction );
-
- Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ),
- helpAboutAction );
-
- menuBar = new MenuBar( fileMenu, editMenu, insertMenu, toolsMenu, helpMenu );
-
- //---- ToolBar ----
- ToolBar toolBar = ActionUtils.createToolBar(
- fileNewAction,
- fileOpenAction,
- fileSaveAction,
- null,
- editUndoAction,
- editRedoAction,
- null,
- insertBoldAction,
- insertItalicAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headers[ 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<FileEditor, ObservableBooleanValue> func ) {
- final BooleanProperty b = new SimpleBooleanProperty();
- final FileEditor fileEditor = getActiveFileEditor();
-
- if( fileEditor != null ) {
- b.bind( func.apply( fileEditor ) );
- }
-
- getFileEditorPane().activeFileEditorProperty().addListener(
- (observable, oldFileEditor, newFileEditor) -> {
- b.unbind();
-
- if( newFileEditor != null ) {
- b.bind( func.apply( newFileEditor ) );
- } else {
- b.set( false );
- }
- } );
-
- return b;
- }
-
- //---- File actions -------------------------------------------------------
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openEditor();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditor(), true );
- }
-
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditor() );
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- Event.fireEvent( window,
- new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Tools actions ------------------------------------------------------
- private void toolsOptions() {
- new OptionsDialog( getWindow() ).showAndWait();
- }
-
- //---- Help actions -------------------------------------------------------
- private void helpAbout() {
- Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( Messages.get( "Dialog.about.title" ) );
- alert.setHeaderText( Messages.get( "Dialog.about.header" ) );
- alert.setContentText( Messages.get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- private FileEditorPane getFileEditorPane() {
- if( this.fileEditorPane == null ) {
- this.fileEditorPane = createFileEditorPane();
- }
-
- return this.fileEditorPane;
- }
-
- private FileEditorPane createFileEditorPane() {
- return new FileEditorPane( this );
- }
-
- private MarkdownEditorPane getActiveEditor() {
- return getActiveFileEditor().getEditorPane();
- }
-
- private FileEditor getActiveFileEditor() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- protected DefinitionPane createDefinitionPane() {
- return new DefinitionPane();
- }
-
- private DefinitionPane getDefinitionPane() {
- if( this.definitionPane == null ) {
- this.definitionPane = createDefinitionPane();
- }
-
- return this.definitionPane;
- }
-
- /**
- * Delegates to the file editor pane, and, ultimately, to its text area.
- */
- private <T extends Event, U extends T> void addEventListener(
- final EventPattern<? super T, ? extends U> event,
- final Consumer<? super U> consumer ) {
- getFileEditorPane().addEventListener( event, consumer );
- }
-
- /**
- * Delegates to the file editor pane, and, ultimately, to its text area.
- *
- * @param map The map of methods to events.
- */
- private void addEventListener( final InputMap<InputEvent> map ) {
- getFileEditorPane().addEventListener( map );
- }
-
- private void removeEventListener( final InputMap<InputEvent> map ) {
- getFileEditorPane().removeEventListener( map );
- }
-
- public MenuBar getMenuBar() {
- return menuBar;
- }
-
- public void setMenuBar( MenuBar menuBar ) {
- this.menuBar = menuBar;
- }
-
- /**
- * Returns the position of the caret when variable mode editing was requested.
- *
- * @return The variable mode caret position.
- */
- private int getVModePosition() {
- return vModePosition;
- }
-
- /**
- * Sets the position of the caret when variable mode editing was requested.
- * Stores the current position because only the text that comes afterwards is
- * a suitable variable reference.
- *
- * @return The variable mode caret position.
- */
- private void setVModePosition() {
- this.vModePosition = getEditor().getCaretColumn();
+import com.scrivendor.editor.VariableEditor;
+import com.scrivendor.options.OptionsDialog;
+import com.scrivendor.util.Action;
+import com.scrivendor.util.ActionUtils;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
+import java.util.function.Function;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.event.Event;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.ToolBar;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import static javafx.scene.input.KeyCode.ESCAPE;
+import javafx.scene.input.KeyEvent;
+import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ *
+ * @author Karl Tauber and White Magic Software, Ltd.
+ */
+public class MainWindow {
+
+ private Scene scene;
+ private FileEditorPane fileEditorPane;
+
+ private DefinitionPane definitionPane;
+ private VariableEditor variableEditor;
+
+ private MenuBar menuBar;
+
+ public MainWindow() {
+ initLayout();
+ initVariableEditor();
+ }
+
+ private void initLayout() {
+ final SplitPane splitPane = new SplitPane(
+ getDefinitionPane().getNode(),
+ getFileEditorPane().getNode() );
+ splitPane.setDividerPositions( .05f, .95f );
+
+ BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1024, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setCenter( splitPane );
+
+ setScene( new Scene( borderPane ) );
+ getScene().getStylesheets().add( Constants.STYLESHEET_PREVIEW );
+ getScene().windowProperty().addListener(
+ (observable, oldWindow, newWindow) -> {
+ newWindow.setOnCloseRequest( e -> {
+ if( !getFileEditorPane().closeAllEditors() ) {
+ e.consume();
+ }
+ } );
+
+ // Workaround JavaFX bug: deselect menubar if window loses focus.
+ newWindow.focusedProperty().addListener(
+ (obs, oldFocused, newFocused) -> {
+ if( !newFocused ) {
+ // Send an ESC key event to the menubar
+ this.menuBar.fireEvent(
+ new KeyEvent(
+ KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
+ false, false, false, false ) );
+ }
+ } );
+ } );
+ }
+
+ private void initVariableEditor() {
+ setVariableEditor( new VariableEditor(
+ getFileEditorPane(),
+ getDefinitionPane() )
+ );
+ }
+
+ private Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ public Scene getScene() {
+ return this.scene;
+ }
+
+ private void setScene( Scene scene ) {
+ this.scene = scene;
+ }
+
+ private Node createMenuBar() {
+ BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
+
+ // File actions
+ Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
+ Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
+ Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
+ Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
+ Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
+ createActiveBooleanProperty( FileEditor::modifiedProperty ).not() );
+ Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
+ Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
+ Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
+
+ // Edit actions
+ Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
+ e -> getActiveEditor().undo(),
+ createActiveBooleanProperty( FileEditor::canUndoProperty ).not() );
+ Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
+ e -> getActiveEditor().redo(),
+ createActiveBooleanProperty( FileEditor::canRedoProperty ).not() );
+
+ // Insert actions
+ Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
+ e -> getActiveEditor().surroundSelection( "**", "**" ),
+ activeFileEditorIsNull );
+ Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
+ e -> getActiveEditor().surroundSelection( "*", "*" ),
+ activeFileEditorIsNull );
+ Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
+ e -> getActiveEditor().surroundSelection( "~~", "~~" ),
+ activeFileEditorIsNull );
+ Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
+ e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
+ activeFileEditorIsNull );
+ Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
+ e -> getActiveEditor().surroundSelection( "`", "`" ),
+ activeFileEditorIsNull );
+ Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
+ e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ),
+ activeFileEditorIsNull );
+
+ Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
+ e -> getActiveEditor().insertLink(),
+ activeFileEditorIsNull );
+ Action insertImageAction = new Action( Messages.get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
+ e -> getActiveEditor().insertImage(),
+ activeFileEditorIsNull );
+
+ final Action[] headers = new Action[ 6 ];
+
+ for( int i = 1; i <= 6; i++ ) {
+ final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
+ final String markup = String.format( "\n\n%s ", hashes );
+ final String text = Messages.get( "Main.menu.insert.header_" + i );
+ final String accelerator = "Shortcut+" + i;
+ final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" );
+
+ headers[ i - 1 ] = new Action( text, accelerator, HEADER,
+ e -> getActiveEditor().surroundSelection( markup, "", prompt ),
+ activeFileEditorIsNull );
+ }
+
+ Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
+ e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
+ activeFileEditorIsNull );
+ Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
+ e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
+ activeFileEditorIsNull );
+ Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
+ e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
+ activeFileEditorIsNull );
+
+ // Tools actions
+ Action toolsOptionsAction = new Action( Messages.get( "Main.menu.tools.options" ), "Shortcut+,", null, e -> toolsOptions() );
+
+ // Help actions
+ Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
+
+ //---- MenuBar ----
+ Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ),
+ fileNewAction,
+ fileOpenAction,
+ null,
+ fileCloseAction,
+ fileCloseAllAction,
+ null,
+ fileSaveAction,
+ fileSaveAllAction,
+ null,
+ fileExitAction );
+
+ Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ),
+ editUndoAction,
+ editRedoAction );
+
+ Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ),
+ insertBoldAction,
+ insertItalicAction,
+ insertStrikethroughAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headers[ 0 ],
+ headers[ 1 ],
+ headers[ 2 ],
+ headers[ 3 ],
+ headers[ 4 ],
+ headers[ 5 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction,
+ insertHorizontalRuleAction );
+
+ Menu toolsMenu = ActionUtils.createMenu( Messages.get( "Main.menu.tools" ),
+ toolsOptionsAction );
+
+ Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ),
+ helpAboutAction );
+
+ menuBar = new MenuBar( fileMenu, editMenu, insertMenu, toolsMenu, helpMenu );
+
+ //---- ToolBar ----
+ ToolBar toolBar = ActionUtils.createToolBar(
+ fileNewAction,
+ fileOpenAction,
+ fileSaveAction,
+ null,
+ editUndoAction,
+ editRedoAction,
+ null,
+ insertBoldAction,
+ insertItalicAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headers[ 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<FileEditor, ObservableBooleanValue> func ) {
+ final BooleanProperty b = new SimpleBooleanProperty();
+ final FileEditor fileEditor = getActiveFileEditor();
+
+ if( fileEditor != null ) {
+ b.bind( func.apply( fileEditor ) );
+ }
+
+ getFileEditorPane().activeFileEditorProperty().addListener(
+ (observable, oldFileEditor, newFileEditor) -> {
+ b.unbind();
+
+ if( newFileEditor != null ) {
+ b.bind( func.apply( newFileEditor ) );
+ } else {
+ b.set( false );
+ }
+ } );
+
+ return b;
+ }
+
+ //---- File actions -------------------------------------------------------
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openEditor();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditor(), true );
+ }
+
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditor() );
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ Event.fireEvent( window,
+ new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Tools actions ------------------------------------------------------
+ private void toolsOptions() {
+ new OptionsDialog( getWindow() ).showAndWait();
+ }
+
+ //---- Help actions -------------------------------------------------------
+ private void helpAbout() {
+ Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( Messages.get( "Dialog.about.title" ) );
+ alert.setHeaderText( Messages.get( "Dialog.about.header" ) );
+ alert.setContentText( Messages.get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ private FileEditorPane getFileEditorPane() {
+ if( this.fileEditorPane == null ) {
+ this.fileEditorPane = createFileEditorPane();
+ }
+
+ return this.fileEditorPane;
+ }
+
+ private FileEditorPane createFileEditorPane() {
+ return new FileEditorPane( this );
+ }
+
+ private MarkdownEditorPane getActiveEditor() {
+ return getActiveFileEditor().getEditorPane();
+ }
+
+ private FileEditor getActiveFileEditor() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ protected DefinitionPane createDefinitionPane() {
+ return new DefinitionPane();
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ if( this.definitionPane == null ) {
+ this.definitionPane = createDefinitionPane();
+ }
+
+ return this.definitionPane;
+ }
+
+ public MenuBar getMenuBar() {
+ return menuBar;
+ }
+
+ public void setMenuBar( MenuBar menuBar ) {
+ this.menuBar = menuBar;
+ }
+
+ public VariableEditor getVariableEditor() {
+ return this.variableEditor;
+ }
+
+ public void setVariableEditor( VariableEditor variableEditor ) {
+ this.variableEditor = variableEditor;
}
}
src/main/java/com/scrivendor/definition/ContainsPredicate.java
+/*
+ * Copyright 2016 White Magic Software, Inc.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivendor.definition;
+
+/**
+ * Determines if one string contains another.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class ContainsPredicate implements Predicate {
+
+ /**
+ * Answers whether the given strings match each other. What match means will
+ * depend on user preferences. The empty condition is required to return the
+ * first node in a list of child nodes when the user has not yet selected a
+ * node.
+ *
+ * @param s1 The string to compare against s2.
+ * @param s2 The string to compare against s1.
+ *
+ * @return true if s1 and s2 are a match according to some criteria,or s2 is
+ * empty.
+ */
+ @Override
+ public boolean pass( final String s1, final String s2 ) {
+ return s1.toLowerCase().contains( s2.toLowerCase() ) || s2.isEmpty();
+ }
+}
src/main/java/com/scrivendor/definition/DefinitionPane.java
import static com.scrivendor.Messages.get;
+import static com.scrivendor.definition.Lists.getFirst;
import com.scrivendor.ui.AbstractPane;
import static com.scrivendor.yaml.YamlTreeAdapter.adapt;
/**
- * Finds a node that matches a prefix and suffix specified by the given
+ * Finds a tree item with a value that exactly matches the given word.
+ *
+ * @param trunk The root item containing a list of nodes to search.
+ * @param word The value of the item to find.
+ *
+ * @return The item that matches the given word, or null if not found.
+ */
+ private TreeItem<String> findNode(
+ final TreeItem<String> trunk,
+ final String word,
+ final Predicate p ) {
+ final List<TreeItem<String>> branches = trunk.getChildren();
+ TreeItem<String> result = null;
+
+ for( final TreeItem<String> leaf : branches ) {
+ if( p.pass( leaf.getValue(), word ) ) {
+ result = leaf;
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Calls findNode with the EqualPredicate.
+ *
+ * @see findNode( TreeItem, String, Predicate )
+ * @return The result from findNode.
+ */
+ private TreeItem<String> findExactNode(
+ final TreeItem<String> trunk,
+ final String word ) {
+ return findNode( trunk, word, new EqualPredicate() );
+ }
+
+ /**
+ * Calls findNode with the ContainsPredicate.
+ *
+ * @see findNode( TreeItem, String, Predicate )
+ * @return The result from findNode.
+ */
+ private TreeItem<String> findPartialNode(
+ final TreeItem<String> trunk,
+ final String word ) {
+ return findNode( trunk, word, new ContainsPredicate() );
+ }
+
+ /**
+ * Finds a node that matches a prefix and suffix specified by the given path
* variable. The prefix must match a valid node value. The suffix refers to
* the start of a string that matches zero or more children of the node
* specified by the prefix.
*
* @param path The word typed by the user, which contains dot-separated node
* names that represent a path within the YAML tree plus a partial variable
* name match (for a node).
*
* @return The node value that starts with the suffix portion of the given
- * path.
+ * path, never null.
*/
- public TreeItem<String> findNode( final String path ) {
+ public TreeItem<String> findNearestNode( final String path ) {
TreeItem<String> cItem = getTreeRoot();
TreeItem<String> pItem = cItem;
final StringTokenizer st = new StringTokenizer( path, getSeparator() );
// Search along a single branch while the tokenized path matches nodes.
while( st.hasMoreTokens() ) {
- if( (cItem = findLeaf( cItem, st.nextToken() )) == null ) {
+ if( (cItem = findExactNode( cItem, st.nextToken() )) == null ) {
break;
}
pItem = cItem;
}
- return pItem.isLeaf() ? pItem.getParent() : pItem;
+ // Attempt to find the node with a name that most closely matches the
+ // unused token.
+ if( st.hasMoreTokens() ) {
+ cItem = findPartialNode( pItem, st.nextToken() );
+
+ // Return a partial match if one was found.
+ if( cItem != null ) {
+ pItem = cItem;
+ }
+ }
+
+ return pItem.getParent() == null
+ ? getFirst( pItem.getChildren(), pItem )
+ : pItem;
}
/**
- * Returns the last word after the separator.
+ * Returns the word after the last separator.
*
* @param path The path to a node, which can include a partial node match.
*
- * @return All characters after the last dot.
+ * @return All characters after the last dot, possibly an empty string, never
+ * null.
*/
- public String findLastWord( final String path ) {
- final int index = path.lastIndexOf( getSeparator() );
- return index > 0 ? path.substring( index + 1 ) : path;
- }
-
+// public String computeLastWord( final String path ) {
+// final int index = path.lastIndexOf( getSeparator() );
+// return index > 0 ? path.substring( index + 1 ) : path;
+// }
/**
* Expands the node to the root, recursively.
*
+ * @param <T> The type of tree item to expand (usually String).
* @param node The node to expand.
*/
- public void expand( TreeItem<String> node ) {
+ public <T> void expand( TreeItem<T> node ) {
if( node != null ) {
expand( node.getParent() );
public void collapse() {
collapse( getTreeRoot().getChildren() );
- }
-
- private void collapse( ObservableList<TreeItem<String>> nodes ) {
- for( final TreeItem<String> node : nodes ) {
- node.setExpanded( false );
- collapse( node.getChildren() );
- }
}
/**
- * Finds a tree item with a value that exactly matches the given word.
- *
- * @param trunk The root item containing a list of nodes to search.
- * @param word The value of the item to find.
+ * Collapses the tree, recursively.
*
- * @return The item that matches the given word, or null if not found.
+ * @param <T> The type of tree item to expand (usually String).
+ * @param node The nodes to collapse.
*/
- private TreeItem<String> findLeaf(
- final TreeItem<String> trunk,
- final String word ) {
- final List<TreeItem<String>> branches = trunk.getChildren();
- TreeItem<String> result = null;
-
- for( final TreeItem<String> leaf : branches ) {
- if( areEqual( leaf.getValue(), word ) ) {
- result = leaf;
- break;
- }
+ private <T> void collapse( ObservableList<TreeItem<T>> nodes ) {
+ for( final TreeItem<T> node : nodes ) {
+ node.setExpanded( false );
+ collapse( node.getChildren() );
}
-
- return result;
- }
-
- /**
- * Compares two strings taking into consideration options for case.
- *
- * @param s1 A non-null string.
- * @param s2
- *
- * @return
- */
- private boolean areEqual( final String s1, final String s2 ) {
- return s1.equalsIgnoreCase( s2 );
- }
-
- /**
- * Answers whether the given strings match each other. What match means will
- * depend on user preferences.
- *
- * @param s1 The string to compare against s2.
- * @param s2 The string to compare against s1.
- *
- * @return true if s1 and s2 are a match according to some criteria.
- */
- public boolean matches( final String s1, final String s2 ) {
- return s1.toLowerCase().contains( s2.toLowerCase() );
}
private TreeItem<String> getTreeRoot() {
return getTreeView().getRoot();
+ }
+
+ public <T> boolean isRoot( final TreeItem<T> item ) {
+ return getTreeRoot().equals( item );
}
}
- private String getSeparator() {
+ public String getSeparator() {
return SEPARATOR;
}
src/main/java/com/scrivendor/definition/EqualPredicate.java
+/*
+ * Copyright 2016 White Magic Software, Inc.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivendor.definition;
+
+/**
+ * Determines if two strings are equal.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class EqualPredicate implements Predicate {
+ /**
+ * Compares two strings taking into consideration options for case.
+ *
+ * @param s1 A non-null string, possibly empty.
+ * @param s2 A non-null string, possibly empty.
+ *
+ * @return true The strings are equal.
+ */
+ @Override
+ public boolean pass( final String s1, final String s2 ) {
+ return s1.equalsIgnoreCase( s2 );
+ }
+}
src/main/java/com/scrivendor/definition/Lists.java
+/*
+ * Copyright 2016 White Magic Software, Inc.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivendor.definition;
+
+import java.util.List;
+
+/**
+ * Convenience class that provides a clearer API for obtaining list elements.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public final class Lists {
+
+ private Lists() {
+ }
+
+ /**
+ * Returns the first item in the given list, or null if not found.
+ *
+ * @param <T> The generic list type.
+ * @param list The list that may have a first item.
+ *
+ * @return null if the list is null or there is no first item.
+ */
+ public static <T> T getFirst( final List<T> list ) {
+ return getFirst( list, null );
+ }
+
+ /**
+ * Returns the last item in the given list, or null if not found.
+ *
+ * @param <T> The generic list type.
+ * @param list The list that may have a last item.
+ *
+ * @return null if the list is null or there is no last item.
+ */
+ public static <T> T getLast( final List<T> list ) {
+ return getLast( list, null );
+ }
+
+ /**
+ * Returns the first item in the given list, or t if not found.
+ *
+ * @param <T> The generic list type.
+ * @param list The list that may have a first item.
+ * @param t The default return value.
+ *
+ * @return null if the list is null or there is no first item.
+ */
+ public static <T> T getFirst( final List<T> list, final T t ) {
+ return isEmpty( list ) ? t : list.get( 0 );
+ }
+
+ /**
+ * Returns the last item in the given list, or t if not found.
+ *
+ * @param <T> The generic list type.
+ * @param list The list that may have a last item.
+ * @param t The default return value.
+ *
+ * @return null if the list is null or there is no last item.
+ */
+ public static <T> T getLast( final List<T> list, final T t ) {
+ return isEmpty( list ) ? t : list.get( list.size() - 1 );
+ }
+
+ /**
+ * Returns true if the given list is null or empty.
+ *
+ * @param <T> The generic list type.
+ * @param list The list that has a last item.
+ *
+ * @return true The list is empty.
+ */
+ public static <T> boolean isEmpty( final List<T> list ) {
+ return list == null || list.isEmpty();
+ }
+}
src/main/java/com/scrivendor/definition/Predicate.java
+/*
+ * Copyright 2016 White Magic Software, Inc.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivendor.definition;
+
+/**
+ * Answers whether two strings may pass through a filter.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public interface Predicate {
+
+ /**
+ * Returns true if the strings match in some subclass-defined criteria.
+ *
+ * @param s1 The string to compare against s2.
+ * @param s2 The string to compare against s1.
+ *
+ * @return true The strings pass the filter test.
+ */
+ public boolean pass( String s1, String s2 );
+}
src/main/java/com/scrivendor/editor/VariableEditor.java
+/*
+ * Copyright 2016 White Magic Software, Inc.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivendor.editor;
+
+import com.scrivendor.FileEditorPane;
+import com.scrivendor.definition.DefinitionPane;
+import static com.scrivendor.definition.Lists.getFirst;
+import static com.scrivendor.definition.Lists.getLast;
+import java.util.function.Consumer;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.scene.control.IndexRange;
+import javafx.scene.control.TreeItem;
+import javafx.scene.input.InputEvent;
+import javafx.scene.input.KeyCode;
+import static javafx.scene.input.KeyCode.DIGIT2;
+import static javafx.scene.input.KeyCode.MINUS;
+import static javafx.scene.input.KeyCode.PERIOD;
+import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
+import javafx.scene.input.KeyEvent;
+import org.fxmisc.richtext.StyledTextArea;
+import org.fxmisc.wellbehaved.event.EventPattern;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
+import org.fxmisc.wellbehaved.event.InputMap;
+import static org.fxmisc.wellbehaved.event.InputMap.consume;
+import static org.fxmisc.wellbehaved.event.InputMap.sequence;
+
+/**
+ * Provides the logic for editing variable names within the editor.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class VariableEditor {
+ /**
+ * Used to capture keyboard events once the user presses @.
+ */
+ private InputMap<InputEvent> keyboardMap;
+
+ private FileEditorPane fileEditorPane;
+ private DefinitionPane definitionPane;
+
+ /**
+ * Position of the variable in the text when in variable mode.
+ */
+ private int initialCaretColumn = 0;
+
+ public VariableEditor(
+ final FileEditorPane editorPane,
+ final DefinitionPane definitionPane ) {
+ setFileEditorPane( editorPane );
+ setDefinitionPane( definitionPane );
+
+ initKeyboardEventListeners();
+ }
+
+ /**
+ * The @ symbol is a short-cut to inserting a YAML variable reference.
+ *
+ * @param e Superfluous information about the key that was pressed.
+ */
+ private void atPressed( KeyEvent e ) {
+ startEventCapture();
+ setInitialCaretColumn();
+ advancePath();
+ }
+
+ /**
+ * Receives key presses until the user completes the variable selection. This
+ * allows the arrow keys to be used for selecting variables.
+ *
+ * @param e The key that was pressed.
+ */
+ private void vModeKeyPressed( KeyEvent e ) {
+ final KeyCode keyCode = e.getCode();
+
+ switch( keyCode ) {
+ case BACK_SPACE:
+ deleteSelection();
+
+ // Break out of variable mode by back spacing to the original position.
+ if( getCurrentCaretColumn() > getInitialCaretColumn() ) {
+ break;
+ }
+
+ case ESCAPE:
+ stopEventCapture();
+ break;
+
+ case PERIOD:
+ case RIGHT:
+ case END:
+ advancePath();
+ break;
+
+ case ENTER:
+ acceptPath();
+ break;
+
+ case UP:
+ cyclePathPrev();
+ break;
+
+ case DOWN:
+ cyclePathNext();
+ break;
+
+ default:
+ if( isVariableNameKey( e ) ) {
+ updateEditorText( e.getText() );
+ }
+
+ break;
+ }
+
+ e.consume();
+ }
+
+ /**
+ * Inserts the string at the current caret position, replacing any selected
+ * text.
+ *
+ * @param text The text to insert, never null.
+ */
+ private void updateEditorText( final String text ) {
+ final StyledTextArea t = getEditor();
+
+ System.out.println( "----------" );
+ System.out.println( "updateText" );
+
+ final int length = text.length();
+ final int posBegan = t.getCaretPosition();
+ final int posEnded = posBegan + length;
+
+ t.replaceSelection( text );
+
+ if( posEnded - posBegan > 0 ) {
+ t.selectRange( posEnded, posBegan );
+ }
+
+ System.out.println( "Inserted: '" + text + "'" );
+ System.out.println( "Selected: '" + t.getSelectedText() + "'" );
+ }
+
+ /**
+ * Updates the text with the path selected (or typed) by the user.
+ */
+ private void advancePath() {
+ System.out.println( "----------" );
+ System.out.println( "advancePath" );
+
+ final String path = getPath();
+ final TreeItem<String> node = findNearestNode( path );
+// final DefinitionPane pane = getDefinitionPane();
+// final String word = pane.computeLastWord( path );
+
+ expand( node );
+
+ String typeAhead = node.getValue();
+
+ if( !node.isLeaf() ) {
+ node.setExpanded( true );
+
+ // TODO: Magically decide how much of the path overlaps the node.
+// final String nodeValue = node.getValue();
+// final int index = nodeValue.indexOf( word );
+//
+// if( index == 0 && !nodeValue.equals( word ) ) {
+// typeAhead = nodeValue.substring( word.length() );
+// }
+ }
+
+ updateEditorText( typeAhead );
+ }
+
+ /**
+ * Called when the user presses either End or Enter key.
+ */
+ private void acceptPath() {
+ final IndexRange range = getSelectionRange();
+ final StyledTextArea textArea = getEditor();
+
+ if( range != null ) {
+ final int rangeEnd = range.getEnd();
+ textArea.deselect();
+ textArea.moveTo( rangeEnd );
+ }
+ }
+
+ /**
+ * Called when the user presses the Backspace key.
+ */
+ private void deleteSelection() {
+ final StyledTextArea textArea = getEditor();
+ textArea.replaceSelection( "" );
+ textArea.deletePreviousChar();
+ }
+
+ /**
+ * Cycles the selected text through the nodes.
+ *
+ * @param direction true - next; false - previous
+ */
+ private void cycleSelection( final boolean direction ) {
+ final String path = getPath();
+ final TreeItem<String> node = findNearestNode( path );
+
+ // Find the sibling for the current selection and replace the current
+ // selection with the sibling's value
+ TreeItem<String> cycled = direction
+ ? node.nextSibling()
+ : node.previousSibling();
+
+ // When cycling at the end (or beginning) of the list, jump to the first
+ // (or last) sibling depending on the cycle direction.
+ if( cycled == null ) {
+ cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
+ }
+
+ expand( cycled );
+ updateEditorText( cycled.getValue() );
+ }
+
+ /**
+ * Cycles to the next sibling of the currently selected tree node.
+ */
+ private void cyclePathNext() {
+ cycleSelection( true );
+ }
+
+ private void cyclePathPrev() {
+ cycleSelection( false );
+ }
+
+ private <T> ObservableList<TreeItem<T>> getSiblings(
+ final TreeItem<T> item ) {
+ final TreeItem<T> parent = item.getParent();
+ return parent == null ? item.getChildren() : parent.getChildren();
+ }
+
+ private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
+ return getFirst( getSiblings( item ), item );
+ }
+
+ private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
+ return getLast( getSiblings( item ), item );
+ }
+
+ /**
+ * Returns the full text for the paragraph that contains the caret.
+ *
+ * @return
+ */
+ private String getCurrentParagraph() {
+ final StyledTextArea textArea = getEditor();
+ final int paragraph = textArea.getCurrentParagraph();
+ return textArea.getText( paragraph );
+ }
+
+ /**
+ * Returns the caret's offset into the current paragraph.
+ *
+ * @return
+ */
+ private int getCurrentCaretColumn() {
+ return getEditor().getCaretColumn();
+ }
+
+ /**
+ * Returns all the characters from the initial caret column to the the first
+ * whitespace character. This will return a path that contains zero or more
+ * separators.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ private String getPath() {
+ final String p = getCurrentParagraph();
+ final String s = p.substring( getInitialCaretColumn() );
+
+ int index = 0;
+
+ while( index < s.length() && !Character.isWhitespace( s.charAt( index ) ) ) {
+ index++;
+ }
+
+ return p.substring( getInitialCaretColumn(), index );
+ }
+
+ /**
+ * Finds the node that most closely matches the given path.
+ *
+ * @param path The path that represents a node.
+ *
+ * @return The node for the path, or the root node if the path could not be
+ * found, but never null.
+ */
+ private TreeItem<String> findNearestNode( final String path ) {
+ return getDefinitionPane().findNearestNode( path );
+ }
+
+ /**
+ * Used to ignore typed keys in favour of trapping pressed keys.
+ *
+ * @param e The key that was typed.
+ */
+ private void vModeKeyTyped( KeyEvent e ) {
+ e.consume();
+ }
+
+ /**
+ * Used to lazily initialize the keyboard map.
+ *
+ * @return Mappings for keyTyped and keyPressed.
+ */
+ protected InputMap<InputEvent> createKeyboardMap() {
+ return sequence(
+ consume( keyTyped(), this::vModeKeyTyped ),
+ consume( keyPressed(), this::vModeKeyPressed )
+ );
+ }
+
+ private InputMap<InputEvent> getKeyboardMap() {
+ if( this.keyboardMap == null ) {
+ this.keyboardMap = createKeyboardMap();
+ }
+
+ return this.keyboardMap;
+ }
+
+ private <T> void expand( final TreeItem<T> node ) {
+ final DefinitionPane pane = getDefinitionPane();
+ pane.collapse();
+ pane.expand( node );
+ }
+
+ /**
+ * Trap the AT key for inserting YAML variables.
+ */
+ private void initKeyboardEventListeners() {
+ addEventListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::atPressed );
+ }
+
+ /**
+ * Returns true iff the key code the user typed can be used as part of a YAML
+ * variable name.
+ *
+ * @param keyEvent Keyboard key press event information.
+ *
+ * @return true The key is a value that can be inserted into the text.
+ */
+ private boolean isVariableNameKey( KeyEvent keyEvent ) {
+ final KeyCode keyCode = keyEvent.getCode();
+
+ return keyCode.isLetterKey()
+ || keyCode.isDigitKey()
+ || keyCode == PERIOD
+ || (keyEvent.isShiftDown() && keyCode == MINUS);
+ }
+
+ /**
+ * Starts to capture user input events.
+ */
+ private void startEventCapture() {
+ addEventListener( getKeyboardMap() );
+ }
+
+ /**
+ * Restores capturing of user input events to the previous event listener.
+ */
+ private void stopEventCapture() {
+ removeEventListener( getKeyboardMap() );
+ }
+
+ /**
+ * Delegates to the file editor pane, and, ultimately, to its text area.
+ */
+ private <T extends Event, U extends T> void addEventListener(
+ final EventPattern<? super T, ? extends U> event,
+ final Consumer<? super U> consumer ) {
+ getFileEditorPane().addEventListener( event, consumer );
+ }
+
+ /**
+ * Delegates to the file editor pane, and, ultimately, to its text area.
+ *
+ * @param map The map of methods to events.
+ */
+ private void addEventListener( final InputMap<InputEvent> map ) {
+ getFileEditorPane().addEventListener( map );
+ }
+
+ private void removeEventListener( final InputMap<InputEvent> map ) {
+ getFileEditorPane().removeEventListener( map );
+ }
+
+ /**
+ * Returns the position of the caret when variable mode editing was requested.
+ *
+ * @return The variable mode caret position.
+ */
+ private int getInitialCaretColumn() {
+ return this.initialCaretColumn;
+ }
+
+ /**
+ * Sets the position of the caret when variable mode editing was requested.
+ * Stores the current position because only the text that comes afterwards is
+ * a suitable variable reference.
+ *
+ * @return The variable mode caret position.
+ */
+ private void setInitialCaretColumn() {
+ this.initialCaretColumn = getEditor().getCaretColumn();
+ }
+
+ private StyledTextArea getEditor() {
+ return getFileEditorPane().getEditor();
+ }
+
+ public FileEditorPane getFileEditorPane() {
+ return this.fileEditorPane;
+ }
+
+ private void setFileEditorPane( final FileEditorPane fileEditorPane ) {
+ this.fileEditorPane = fileEditorPane;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ return this.definitionPane;
+ }
+
+ private void setDefinitionPane( final DefinitionPane definitionPane ) {
+ this.definitionPane = definitionPane;
+ }
+
+ private IndexRange getSelectionRange() {
+ return getEditor().getSelection();
+ }
+}
Delta1202 lines added, 748 lines removed, 454-line increase