| | import javafx.scene.Node; |
| | import javafx.scene.control.*; |
| | -import javafx.scene.control.cell.TextFieldTreeCell; |
| | -import javafx.scene.input.KeyEvent; |
| | -import javafx.scene.layout.BorderPane; |
| | -import javafx.scene.layout.HBox; |
| | -import javafx.util.StringConverter; |
| | - |
| | -import java.util.*; |
| | - |
| | -import static com.scrivenvar.Messages.get; |
| | -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; |
| | -import static javafx.geometry.Pos.CENTER; |
| | -import static javafx.scene.input.KeyEvent.KEY_PRESSED; |
| | - |
| | -/** |
| | - * Provides the user interface that holds a {@link TreeView}, which |
| | - * allows users to interact with key/value pairs loaded from the |
| | - * {@link DocumentParser} and adapted using a {@link TreeAdapter}. |
| | - */ |
| | -public final class DefinitionPane extends BorderPane { |
| | - |
| | - /** |
| | - * Contains a view of the definitions. |
| | - */ |
| | - private final TreeView<String> mTreeView = new TreeView<>(); |
| | - |
| | - /** |
| | - * Handlers for key press events. |
| | - */ |
| | - private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers |
| | - = new HashSet<>(); |
| | - |
| | - /** |
| | - * Definition file name shown in the title of the pane. |
| | - */ |
| | - private final StringProperty mFilename = new SimpleStringProperty(); |
| | - |
| | - private final TitledPane mTitledPane = new TitledPane(); |
| | - |
| | - /** |
| | - * Constructs a definition pane with a given tree view root. |
| | - */ |
| | - public DefinitionPane() { |
| | - final var treeView = getTreeView(); |
| | - treeView.setEditable( true ); |
| | - treeView.setCellFactory( cell -> createTreeCell() ); |
| | - treeView.setContextMenu( createContextMenu() ); |
| | - treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter ); |
| | - treeView.setShowRoot( false ); |
| | - getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE ); |
| | - |
| | - final var bCreate = createButton( |
| | - "create", TREE, e -> addItem() ); |
| | - final var bRename = createButton( |
| | - "rename", EDIT, e -> editSelectedItem() ); |
| | - final var bDelete = createButton( |
| | - "delete", TRASH, e -> deleteSelectedItems() ); |
| | - |
| | - final var buttonBar = new HBox(); |
| | - buttonBar.getChildren().addAll( bCreate, bRename, bDelete ); |
| | - buttonBar.setAlignment( CENTER ); |
| | - buttonBar.setSpacing( 10 ); |
| | - |
| | - final var titledPane = getTitledPane(); |
| | - titledPane.textProperty().bind( mFilename ); |
| | - titledPane.setContent( treeView ); |
| | - titledPane.setCollapsible( false ); |
| | - titledPane.setPadding( new Insets( 0, 0, 0, 0 ) ); |
| | - |
| | - setTop( buttonBar ); |
| | - setCenter( titledPane ); |
| | - setAlignment( buttonBar, Pos.TOP_CENTER ); |
| | - setAlignment( titledPane, Pos.TOP_CENTER ); |
| | - |
| | - titledPane.prefHeightProperty().bind( this.heightProperty() ); |
| | - } |
| | - |
| | - public void setTooltip( final Tooltip tooltip ) { |
| | - getTitledPane().setTooltip( tooltip ); |
| | - } |
| | - |
| | - private TitledPane getTitledPane() { |
| | - return mTitledPane; |
| | - } |
| | - |
| | - private Button createButton( |
| | - final String msgKey, |
| | - final FontAwesomeIcon icon, |
| | - final EventHandler<ActionEvent> eventHandler ) { |
| | - final var keyPrefix = "Pane.definition.button." + msgKey; |
| | - final var button = new Button( get( keyPrefix + ".label" ) ); |
| | - button.setOnAction( eventHandler ); |
| | - |
| | - button.setGraphic( |
| | - FontAwesomeIconFactory.get().createIcon( icon ) |
| | - ); |
| | - button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) ); |
| | - |
| | - return button; |
| | - } |
| | - |
| | - /** |
| | - * Changes the root of the {@link TreeView} to the root of the |
| | - * {@link TreeView} from the {@link DefinitionSource}. |
| | - * |
| | - * @param definitionSource Container for the hierarchy of key/value pairs |
| | - * to replace the existing hierarchy. |
| | - */ |
| | - public void update( final DefinitionSource definitionSource ) { |
| | - assert definitionSource != null; |
| | - |
| | - final TreeAdapter treeAdapter = definitionSource.getTreeAdapter(); |
| | - final TreeItem<String> root = treeAdapter.adapt( |
| | - get( "Pane.definition.node.root.title" ) |
| | - ); |
| | - |
| | - getTreeView().setRoot( root ); |
| | - } |
| | - |
| | - public Map<String, String> toMap() { |
| | - return TreeItemAdapter.toMap( getTreeView().getRoot() ); |
| | - } |
| | - |
| | - /** |
| | - * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView} |
| | - * is modified. The modifications include: item value changes, item additions, |
| | - * and item removals. |
| | - * <p> |
| | - * Safe to call multiple times; if a handler is already registered, the |
| | - * old handler is used. |
| | - * </p> |
| | - * |
| | - * @param handler The handler to call whenever any {@link TreeItem} changes. |
| | - */ |
| | - public void addTreeChangeHandler( |
| | - final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) { |
| | - final TreeItem<String> root = getTreeView().getRoot(); |
| | - root.addEventHandler( TreeItem.valueChangedEvent(), handler ); |
| | - root.addEventHandler( TreeItem.childrenModificationEvent(), handler ); |
| | - } |
| | - |
| | - public void addKeyEventHandler( |
| | - final EventHandler<? super KeyEvent> handler ) { |
| | - getKeyEventHandlers().add( handler ); |
| | - } |
| | - |
| | - /** |
| | - * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably |
| | - * well-formed for export. A tree is considered well-formed if the following |
| | - * conditions are met: |
| | - * |
| | - * <ul> |
| | - * <li>The root node contains at least one child node having a leaf.</li> |
| | - * <li>There are no leaf nodes with sibling leaf nodes.</li> |
| | - * </ul> |
| | - * |
| | - * @return {@code null} if the document is well-formed, otherwise the |
| | - * problematic child {@link TreeItem}. |
| | - */ |
| | - public TreeItem<String> isTreeWellFormed() { |
| | - final var root = getTreeView().getRoot(); |
| | - |
| | - for( final var child : root.getChildren() ) { |
| | - final var problemChild = isWellFormed( child ); |
| | - |
| | - if( child.isLeaf() || problemChild != null ) { |
| | - return problemChild; |
| | - } |
| | - } |
| | - |
| | - return null; |
| | - } |
| | - |
| | - /** |
| | - * Determines whether the document is well-formed by ensuring that |
| | - * child branches do not contain multiple leaves. |
| | - * |
| | - * @param item The sub-tree to check for well-formedness. |
| | - * @return {@code null} when the tree is well-formed, otherwise the |
| | - * problematic {@link TreeItem}. |
| | - */ |
| | - private TreeItem<String> isWellFormed( final TreeItem<String> item ) { |
| | - int childLeafs = 0; |
| | - int childBranches = 0; |
| | - |
| | - for( final TreeItem<String> child : item.getChildren() ) { |
| | - if( child.isLeaf() ) { |
| | - childLeafs++; |
| | - } |
| | - else { |
| | - childBranches++; |
| | - } |
| | - |
| | - final var problemChild = isWellFormed( child ); |
| | - |
| | - if( problemChild != null ) { |
| | - return problemChild; |
| | - } |
| | - } |
| | - |
| | - return ((childBranches > 0 && childLeafs == 0) || |
| | - (childBranches == 0 && childLeafs <= 1)) ? null : item; |
| | - } |
| | - |
| | - /** |
| | - * Delegates to {@link VariableTreeItem#findLeafExact(String)}. |
| | - * |
| | - * @param text The value to find, never {@code null}. |
| | - * @return The leaf that contains the given value, or {@code null} if |
| | - * not found. |
| | - */ |
| | - public VariableTreeItem<String> findLeafExact( final String text ) { |
| | - return getTreeRoot().findLeafExact( text ); |
| | - } |
| | - |
| | - /** |
| | - * Delegates to {@link VariableTreeItem#findLeafContains(String)}. |
| | - * |
| | - * @param text The value to find, never {@code null}. |
| | - * @return The leaf that contains the given value, or {@code null} if |
| | - * not found. |
| | - */ |
| | - public VariableTreeItem<String> findLeafContains( final String text ) { |
| | - return getTreeRoot().findLeafContains( text ); |
| | - } |
| | - |
| | - /** |
| | - * Delegates to {@link VariableTreeItem#findLeafContains(String)}. |
| | - * |
| | - * @param text The value to find, never {@code null}. |
| | - * @return The leaf that contains the given value, or {@code null} if |
| | - * not found. |
| | - */ |
| | - public VariableTreeItem<String> findLeafContainsNoCase( final String text ) { |
| | - return getTreeRoot().findLeafContainsNoCase( text ); |
| | - } |
| | - |
| | - /** |
| | - * Delegates to {@link VariableTreeItem#findLeafStartsWith(String)}. |
| | - * |
| | - * @param text The value to find, never {@code null}. |
| | - * @return The leaf that contains the given value, or {@code null} if |
| | - * not found. |
| | - */ |
| | - public VariableTreeItem<String> findLeafStartsWith( final String text ) { |
| | - return getTreeRoot().findLeafStartsWith( text ); |
| | - } |
| | - |
| | - /** |
| | - * 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 <T> void expand( final TreeItem<T> node ) { |
| | - if( node != null ) { |
| | - expand( node.getParent() ); |
| | - |
| | - if( !node.isLeaf() ) { |
| | - node.setExpanded( true ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - public void select( final TreeItem<String> item ) { |
| | - getSelectionModel().clearSelection(); |
| | - getSelectionModel().select( getTreeView().getRow( item ) ); |
| | - } |
| | - |
| | - /** |
| | - * Collapses the tree, recursively. |
| | - */ |
| | - public void collapse() { |
| | - collapse( getTreeRoot().getChildren() ); |
| | - } |
| | - |
| | - /** |
| | - * Collapses the tree, recursively. |
| | - * |
| | - * @param <T> The type of tree item to expand (usually String). |
| | - * @param nodes The nodes to collapse. |
| | - */ |
| | - private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) { |
| | - for( final var node : nodes ) { |
| | - node.setExpanded( false ); |
| | - collapse( node.getChildren() ); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * @return {@code true} when the user is editing a {@link TreeItem}. |
| | - */ |
| | - private boolean isEditingTreeItem() { |
| | - return getTreeView().editingItemProperty().getValue() != null; |
| | - } |
| | - |
| | - /** |
| | - * Changes to edit mode for the selected item. |
| | - */ |
| | - private void editSelectedItem() { |
| | - getTreeView().edit( getSelectedItem() ); |
| | - } |
| | - |
| | - /** |
| | - * Removes all selected items from the {@link TreeView}. |
| | - */ |
| | - private void deleteSelectedItems() { |
| | - for( final var item : getSelectedItems() ) { |
| | - final var parent = item.getParent(); |
| | - |
| | - if( parent != null ) { |
| | - parent.getChildren().remove( item ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Deletes the selected item. |
| | - */ |
| | - private void deleteSelectedItem() { |
| | - final var c = getSelectedItem(); |
| | - getSiblings( c ).remove( c ); |
| | - } |
| | - |
| | - /** |
| | - * Adds a new item under the selected item (or root if nothing is selected). |
| | - * There are a few conditions to consider: when adding to the root, |
| | - * when adding to a leaf, and when adding to a non-leaf. Items added to the |
| | - * root must contain two items: a key and a value. |
| | - */ |
| | - private void addItem() { |
| | - final var value = createTreeItem(); |
| | - getSelectedItem().getChildren().add( value ); |
| | - expand( value ); |
| | - select( value ); |
| | - } |
| | - |
| | - private ContextMenu createContextMenu() { |
| | - final ContextMenu menu = new ContextMenu(); |
| | - final ObservableList<MenuItem> items = menu.getItems(); |
| | - |
| | - addMenuItem( items, "Definition.menu.create" ) |
| | - .setOnAction( e -> addItem() ); |
| | - |
| | - addMenuItem( items, "Definition.menu.rename" ) |
| | - .setOnAction( e -> editSelectedItem() ); |
| | - |
| | - addMenuItem( items, "Definition.menu.remove" ) |
| | - .setOnAction( e -> deleteSelectedItem() ); |
| | - |
| | - return menu; |
| | - } |
| | - |
| | - /** |
| | - * Executes hot-keys for edits to the definition tree. |
| | - * |
| | - * @param event Contains the key code of the key that was pressed. |
| | - */ |
| | - private void keyEventFilter( final KeyEvent event ) { |
| | - if( !isEditingTreeItem() ) { |
| | - switch( event.getCode() ) { |
| | - case ENTER: |
| | - expand( getSelectedItem() ); |
| | - event.consume(); |
| | - break; |
| | - |
| | - case DELETE: |
| | - deleteSelectedItems(); |
| | - break; |
| | - |
| | - case INSERT: |
| | - addItem(); |
| | - break; |
| | - |
| | - case R: |
| | - if( event.isControlDown() ) { |
| | - editSelectedItem(); |
| | - } |
| | - |
| | - break; |
| | - } |
| | - |
| | - for( final var handler : getKeyEventHandlers() ) { |
| | - handler.handle( event ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Adds a menu item to a list of menu items. |
| | - * |
| | - * @param items The list of menu items to append to. |
| | - * @param labelKey The resource bundle key name for the menu item's label. |
| | - * @return The menu item added to the list of menu items. |
| | - */ |
| | - private MenuItem addMenuItem( |
| | - final List<MenuItem> items, final String labelKey ) { |
| | - final MenuItem menuItem = createMenuItem( labelKey ); |
| | - items.add( menuItem ); |
| | - return menuItem; |
| | - } |
| | - |
| | - private MenuItem createMenuItem( final String labelKey ) { |
| | - return new MenuItem( get( labelKey ) ); |
| | - } |
| | - |
| | - private VariableTreeItem<String> createTreeItem() { |
| | - return new VariableTreeItem<>( get( "Definition.menu.add.default" ) ); |
| | - } |
| | - |
| | - private TreeCell<String> createTreeCell() { |
| | - return new TextFieldTreeCell<>( createStringConverter() ) { |
| | +import javafx.scene.input.KeyEvent; |
| | +import javafx.scene.layout.BorderPane; |
| | +import javafx.scene.layout.HBox; |
| | +import javafx.util.StringConverter; |
| | + |
| | +import java.util.*; |
| | + |
| | +import static com.scrivenvar.Messages.get; |
| | +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; |
| | +import static javafx.geometry.Pos.CENTER; |
| | +import static javafx.scene.input.KeyEvent.KEY_PRESSED; |
| | + |
| | +/** |
| | + * Provides the user interface that holds a {@link TreeView}, which |
| | + * allows users to interact with key/value pairs loaded from the |
| | + * {@link DocumentParser} and adapted using a {@link TreeAdapter}. |
| | + */ |
| | +public final class DefinitionPane extends BorderPane { |
| | + |
| | + /** |
| | + * Contains a view of the definitions. |
| | + */ |
| | + private final TreeView<String> mTreeView = new TreeView<>(); |
| | + |
| | + /** |
| | + * Handlers for key press events. |
| | + */ |
| | + private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers |
| | + = new HashSet<>(); |
| | + |
| | + /** |
| | + * Definition file name shown in the title of the pane. |
| | + */ |
| | + private final StringProperty mFilename = new SimpleStringProperty(); |
| | + |
| | + private final TitledPane mTitledPane = new TitledPane(); |
| | + |
| | + /** |
| | + * Constructs a definition pane with a given tree view root. |
| | + */ |
| | + public DefinitionPane() { |
| | + final var treeView = getTreeView(); |
| | + treeView.setEditable( true ); |
| | + treeView.setCellFactory( cell -> createTreeCell() ); |
| | + treeView.setContextMenu( createContextMenu() ); |
| | + treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter ); |
| | + treeView.setShowRoot( false ); |
| | + getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE ); |
| | + |
| | + final var bCreate = createButton( |
| | + "create", TREE, e -> addItem() ); |
| | + final var bRename = createButton( |
| | + "rename", EDIT, e -> editSelectedItem() ); |
| | + final var bDelete = createButton( |
| | + "delete", TRASH, e -> deleteSelectedItems() ); |
| | + |
| | + final var buttonBar = new HBox(); |
| | + buttonBar.getChildren().addAll( bCreate, bRename, bDelete ); |
| | + buttonBar.setAlignment( CENTER ); |
| | + buttonBar.setSpacing( 10 ); |
| | + |
| | + final var titledPane = getTitledPane(); |
| | + titledPane.textProperty().bind( mFilename ); |
| | + titledPane.setContent( treeView ); |
| | + titledPane.setCollapsible( false ); |
| | + titledPane.setPadding( new Insets( 0, 0, 0, 0 ) ); |
| | + |
| | + setTop( buttonBar ); |
| | + setCenter( titledPane ); |
| | + setAlignment( buttonBar, Pos.TOP_CENTER ); |
| | + setAlignment( titledPane, Pos.TOP_CENTER ); |
| | + |
| | + titledPane.prefHeightProperty().bind( this.heightProperty() ); |
| | + } |
| | + |
| | + public void setTooltip( final Tooltip tooltip ) { |
| | + getTitledPane().setTooltip( tooltip ); |
| | + } |
| | + |
| | + private TitledPane getTitledPane() { |
| | + return mTitledPane; |
| | + } |
| | + |
| | + private Button createButton( |
| | + final String msgKey, |
| | + final FontAwesomeIcon icon, |
| | + final EventHandler<ActionEvent> eventHandler ) { |
| | + final var keyPrefix = "Pane.definition.button." + msgKey; |
| | + final var button = new Button( get( keyPrefix + ".label" ) ); |
| | + button.setOnAction( eventHandler ); |
| | + |
| | + button.setGraphic( |
| | + FontAwesomeIconFactory.get().createIcon( icon ) |
| | + ); |
| | + button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) ); |
| | + |
| | + return button; |
| | + } |
| | + |
| | + /** |
| | + * Changes the root of the {@link TreeView} to the root of the |
| | + * {@link TreeView} from the {@link DefinitionSource}. |
| | + * |
| | + * @param definitionSource Container for the hierarchy of key/value pairs |
| | + * to replace the existing hierarchy. |
| | + */ |
| | + public void update( final DefinitionSource definitionSource ) { |
| | + assert definitionSource != null; |
| | + |
| | + final TreeAdapter treeAdapter = definitionSource.getTreeAdapter(); |
| | + final TreeItem<String> root = treeAdapter.adapt( |
| | + get( "Pane.definition.node.root.title" ) |
| | + ); |
| | + |
| | + getTreeView().setRoot( root ); |
| | + } |
| | + |
| | + public Map<String, String> toMap() { |
| | + return TreeItemAdapter.toMap( getTreeView().getRoot() ); |
| | + } |
| | + |
| | + /** |
| | + * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView} |
| | + * is modified. The modifications include: item value changes, item additions, |
| | + * and item removals. |
| | + * <p> |
| | + * Safe to call multiple times; if a handler is already registered, the |
| | + * old handler is used. |
| | + * </p> |
| | + * |
| | + * @param handler The handler to call whenever any {@link TreeItem} changes. |
| | + */ |
| | + public void addTreeChangeHandler( |
| | + final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) { |
| | + final TreeItem<String> root = getTreeView().getRoot(); |
| | + root.addEventHandler( TreeItem.valueChangedEvent(), handler ); |
| | + root.addEventHandler( TreeItem.childrenModificationEvent(), handler ); |
| | + } |
| | + |
| | + public void addKeyEventHandler( |
| | + final EventHandler<? super KeyEvent> handler ) { |
| | + getKeyEventHandlers().add( handler ); |
| | + } |
| | + |
| | + /** |
| | + * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably |
| | + * well-formed for export. A tree is considered well-formed if the following |
| | + * conditions are met: |
| | + * |
| | + * <ul> |
| | + * <li>The root node contains at least one child node having a leaf.</li> |
| | + * <li>There are no leaf nodes with sibling leaf nodes.</li> |
| | + * </ul> |
| | + * |
| | + * @return {@code null} if the document is well-formed, otherwise the |
| | + * problematic child {@link TreeItem}. |
| | + */ |
| | + public TreeItem<String> isTreeWellFormed() { |
| | + final var root = getTreeView().getRoot(); |
| | + |
| | + for( final var child : root.getChildren() ) { |
| | + final var problemChild = isWellFormed( child ); |
| | + |
| | + if( child.isLeaf() || problemChild != null ) { |
| | + return problemChild; |
| | + } |
| | + } |
| | + |
| | + return null; |
| | + } |
| | + |
| | + /** |
| | + * Determines whether the document is well-formed by ensuring that |
| | + * child branches do not contain multiple leaves. |
| | + * |
| | + * @param item The sub-tree to check for well-formedness. |
| | + * @return {@code null} when the tree is well-formed, otherwise the |
| | + * problematic {@link TreeItem}. |
| | + */ |
| | + private TreeItem<String> isWellFormed( final TreeItem<String> item ) { |
| | + int childLeafs = 0; |
| | + int childBranches = 0; |
| | + |
| | + for( final TreeItem<String> child : item.getChildren() ) { |
| | + if( child.isLeaf() ) { |
| | + childLeafs++; |
| | + } |
| | + else { |
| | + childBranches++; |
| | + } |
| | + |
| | + final var problemChild = isWellFormed( child ); |
| | + |
| | + if( problemChild != null ) { |
| | + return problemChild; |
| | + } |
| | + } |
| | + |
| | + return ((childBranches > 0 && childLeafs == 0) || |
| | + (childBranches == 0 && childLeafs <= 1)) ? null : item; |
| | + } |
| | + |
| | + /** |
| | + * Delegates to {@link VariableTreeItem#findLeafExact(String)}. |
| | + * |
| | + * @param text The value to find, never {@code null}. |
| | + * @return The leaf that contains the given value, or {@code null} if |
| | + * not found. |
| | + */ |
| | + public VariableTreeItem<String> findLeafExact( final String text ) { |
| | + return getTreeRoot().findLeafExact( text ); |
| | + } |
| | + |
| | + /** |
| | + * Delegates to {@link VariableTreeItem#findLeafContains(String)}. |
| | + * |
| | + * @param text The value to find, never {@code null}. |
| | + * @return The leaf that contains the given value, or {@code null} if |
| | + * not found. |
| | + */ |
| | + public VariableTreeItem<String> findLeafContains( final String text ) { |
| | + return getTreeRoot().findLeafContains( text ); |
| | + } |
| | + |
| | + /** |
| | + * Delegates to {@link VariableTreeItem#findLeafContains(String)}. |
| | + * |
| | + * @param text The value to find, never {@code null}. |
| | + * @return The leaf that contains the given value, or {@code null} if |
| | + * not found. |
| | + */ |
| | + public VariableTreeItem<String> findLeafContainsNoCase( final String text ) { |
| | + return getTreeRoot().findLeafContainsNoCase( text ); |
| | + } |
| | + |
| | + /** |
| | + * Delegates to {@link VariableTreeItem#findLeafStartsWith(String)}. |
| | + * |
| | + * @param text The value to find, never {@code null}. |
| | + * @return The leaf that contains the given value, or {@code null} if |
| | + * not found. |
| | + */ |
| | + public VariableTreeItem<String> findLeafStartsWith( final String text ) { |
| | + return getTreeRoot().findLeafStartsWith( text ); |
| | + } |
| | + |
| | + /** |
| | + * 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 <T> void expand( final TreeItem<T> node ) { |
| | + if( node != null ) { |
| | + expand( node.getParent() ); |
| | + |
| | + if( !node.isLeaf() ) { |
| | + node.setExpanded( true ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + public void select( final TreeItem<String> item ) { |
| | + getSelectionModel().clearSelection(); |
| | + getSelectionModel().select( getTreeView().getRow( item ) ); |
| | + } |
| | + |
| | + /** |
| | + * Collapses the tree, recursively. |
| | + */ |
| | + public void collapse() { |
| | + collapse( getTreeRoot().getChildren() ); |
| | + } |
| | + |
| | + /** |
| | + * Collapses the tree, recursively. |
| | + * |
| | + * @param <T> The type of tree item to expand (usually String). |
| | + * @param nodes The nodes to collapse. |
| | + */ |
| | + private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) { |
| | + for( final var node : nodes ) { |
| | + node.setExpanded( false ); |
| | + collapse( node.getChildren() ); |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * @return {@code true} when the user is editing a {@link TreeItem}. |
| | + */ |
| | + private boolean isEditingTreeItem() { |
| | + return getTreeView().editingItemProperty().getValue() != null; |
| | + } |
| | + |
| | + /** |
| | + * Changes to edit mode for the selected item. |
| | + */ |
| | + private void editSelectedItem() { |
| | + getTreeView().edit( getSelectedItem() ); |
| | + } |
| | + |
| | + /** |
| | + * Removes all selected items from the {@link TreeView}. |
| | + */ |
| | + private void deleteSelectedItems() { |
| | + for( final var item : getSelectedItems() ) { |
| | + final var parent = item.getParent(); |
| | + |
| | + if( parent != null ) { |
| | + parent.getChildren().remove( item ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Deletes the selected item. |
| | + */ |
| | + private void deleteSelectedItem() { |
| | + final var c = getSelectedItem(); |
| | + getSiblings( c ).remove( c ); |
| | + } |
| | + |
| | + /** |
| | + * Adds a new item under the selected item (or root if nothing is selected). |
| | + * There are a few conditions to consider: when adding to the root, |
| | + * when adding to a leaf, and when adding to a non-leaf. Items added to the |
| | + * root must contain two items: a key and a value. |
| | + */ |
| | + private void addItem() { |
| | + final var value = createTreeItem(); |
| | + getSelectedItem().getChildren().add( value ); |
| | + expand( value ); |
| | + select( value ); |
| | + } |
| | + |
| | + private ContextMenu createContextMenu() { |
| | + final ContextMenu menu = new ContextMenu(); |
| | + final ObservableList<MenuItem> items = menu.getItems(); |
| | + |
| | + addMenuItem( items, "Definition.menu.create" ) |
| | + .setOnAction( e -> addItem() ); |
| | + |
| | + addMenuItem( items, "Definition.menu.rename" ) |
| | + .setOnAction( e -> editSelectedItem() ); |
| | + |
| | + addMenuItem( items, "Definition.menu.remove" ) |
| | + .setOnAction( e -> deleteSelectedItem() ); |
| | + |
| | + return menu; |
| | + } |
| | + |
| | + /** |
| | + * Executes hot-keys for edits to the definition tree. |
| | + * |
| | + * @param event Contains the key code of the key that was pressed. |
| | + */ |
| | + private void keyEventFilter( final KeyEvent event ) { |
| | + if( !isEditingTreeItem() ) { |
| | + switch( event.getCode() ) { |
| | + case ENTER: |
| | + expand( getSelectedItem() ); |
| | + event.consume(); |
| | + break; |
| | + |
| | + case DELETE: |
| | + deleteSelectedItems(); |
| | + break; |
| | + |
| | + case INSERT: |
| | + addItem(); |
| | + break; |
| | + |
| | + case R: |
| | + if( event.isControlDown() ) { |
| | + editSelectedItem(); |
| | + } |
| | + |
| | + break; |
| | + } |
| | + |
| | + for( final var handler : getKeyEventHandlers() ) { |
| | + handler.handle( event ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Adds a menu item to a list of menu items. |
| | + * |
| | + * @param items The list of menu items to append to. |
| | + * @param labelKey The resource bundle key name for the menu item's label. |
| | + * @return The menu item added to the list of menu items. |
| | + */ |
| | + private MenuItem addMenuItem( |
| | + final List<MenuItem> items, final String labelKey ) { |
| | + final MenuItem menuItem = createMenuItem( labelKey ); |
| | + items.add( menuItem ); |
| | + return menuItem; |
| | + } |
| | + |
| | + private MenuItem createMenuItem( final String labelKey ) { |
| | + return new MenuItem( get( labelKey ) ); |
| | + } |
| | + |
| | + private VariableTreeItem<String> createTreeItem() { |
| | + return new VariableTreeItem<>( get( "Definition.menu.add.default" ) ); |
| | + } |
| | + |
| | + private TreeCell<String> createTreeCell() { |
| | + return new FocusAwareTextFieldTreeCell( createStringConverter() ) { |
| | @Override |
| | public void commitEdit( final String newValue ) { |