/* * Copyright 2016 White Magic Software, Ltd. * * 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.scrivenvar.definition; import com.scrivenvar.AbstractPane; import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR; import com.scrivenvar.predicates.strings.ContainsPredicate; import com.scrivenvar.predicates.strings.StartsPredicate; import com.scrivenvar.predicates.strings.StringPredicate; import static com.scrivenvar.util.Lists.getFirst; import java.util.List; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.event.EventType; import javafx.scene.Node; import javafx.scene.control.MultipleSelectionModel; import javafx.scene.control.SelectionMode; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.input.MouseButton; import static javafx.scene.input.MouseButton.PRIMARY; import javafx.scene.input.MouseEvent; import static javafx.scene.input.MouseEvent.MOUSE_CLICKED; /** * Provides a list of variables that can be referenced in the editor. * * @author White Magic Software, Ltd. */ public class DefinitionPane extends AbstractPane { /** * Trimmed off the end of a word to match a variable name. */ private final static String TERMINALS = ":;,.!?-/\\¡¿"; private TreeView<String> treeView; /** * Constructs a definition pane with a given tree view root. * * @see YamlTreeAdapter.adapt * @param root The root of the variable definition tree. */ public DefinitionPane( final TreeView<String> root ) { setTreeView( root ); initTreeView(); } /** * Allows observers to receive double-click events on the tree view. * * @param handler The handler that will receive double-click events. */ public void addBranchSelectedListener( final EventHandler<? super MouseEvent> handler ) { getTreeView().addEventHandler( MouseEvent.ANY, event -> { final MouseButton button = event.getButton(); final int clicks = event.getClickCount(); final EventType<? extends MouseEvent> eventType = event.getEventType(); if( PRIMARY.equals( button ) && clicks == 2 ) { if( MOUSE_CLICKED.equals( eventType ) ) { handler.handle( event ); } event.consume(); } } ); } /** * Allows observers to stop receiving double-click events on the tree view. * * @param handler The handler that will no longer receive double-click events. */ public void removeBranchSelectedListener( final EventHandler<? super MouseEvent> handler ) { getTreeView().removeEventHandler( MouseEvent.ANY, handler ); } /** * Changes the root node of the tree view. Swaps the current root node for the * root node of the given * * @param treeView The tree view containing a new root node; if the parameter * is null, the tree is cleared. */ public void setRoot( final TreeView<String> treeView ) { getTreeView().setRoot( treeView == null ? null : treeView.getRoot() ); } /** * Clears the tree view by setting the root node to null. */ public void clear() { setRoot( null ); } /** * 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. * @param predicate Helps determine whether the node value matches the word. * * @return The item that matches the given word, or null if not found. */ private TreeItem<String> findNode( final TreeItem<String> trunk, final StringPredicate predicate ) { TreeItem<String> result = null; if( trunk != null ) { final List<TreeItem<String>> branches = trunk.getChildren(); for( final TreeItem<String> leaf : branches ) { if( predicate.test( leaf.getValue() ) ) { result = leaf; break; } } } return result; } /** * Calls findNode with the EqualsPredicate. * * @see findNode( TreeItem, String, Predicate ) * @return The result from findNode. */ private TreeItem<String> findStartsNode( final TreeItem<String> trunk, final String word ) { return findNode( trunk, new StartsPredicate( word ) ); } /** * Calls findNode with the ContainsPredicate. * * @see findNode( TreeItem, String, Predicate ) * @return The result from findNode. */ private TreeItem<String> findSubstringNode( final TreeItem<String> trunk, final String word ) { return findNode( trunk, new ContainsPredicate( word ) ); } /** * 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. The algorithm has the following cases: * * <ol> * <li>Path is empty, return first child.</li> * <li>Path contains a complete match, return corresponding node.</li> * <li>Path contains a partial match, return nearest node.</li> * <li>Path contains a complete and partial match, return nearest node.</li> * </ol> * * @param word 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, never null. */ public TreeItem<String> findNode( final String word ) { String path = word; // Current tree item. TreeItem<String> cItem = getTreeRoot(); // Previous tree item. TreeItem<String> pItem = cItem; int index = path.indexOf( SEPARATOR_CHAR ); while( index >= 0 ) { final String node = path.substring( 0, index ); path = path.substring( index + 1 ); if( (cItem = findStartsNode( cItem, node )) == null ) { break; } index = path.indexOf( SEPARATOR_CHAR ); pItem = cItem; } // Find the node that starts with whatever the user typed. cItem = findStartsNode( pItem, path ); // If there was no matching node, then find a substring match. if( cItem == null ) { cItem = findSubstringNode( pItem, path ); } // If neither starts with nor substring matched a node, revert to the last // known valid node. if( cItem == null ) { cItem = pItem; } return sanitize( cItem ); } /** * Returns the leaf that matches the given value. If the value is terminally * punctuated, the punctuation is removed if no match was found. * * @param value The value to find, never null. * * @return The leaf that contains the given value, or null if neither the * original value nor the terminally-trimmed value was found. */ public VariableTreeItem<String> findLeaf( final String value ) { return findLeaf( value, false ); } /** * Returns the leaf that matches the given value. If the value is terminally * punctuated, the punctuation is removed if no match was found. * * @param value The value to find, never null. * @param contains Set to true to perform a substring match if starts with * fails to match. * * @return The leaf that contains the given value, or null if neither the * original value nor the terminally-trimmed value was found. */ public VariableTreeItem<String> findLeaf( final String value, final boolean contains ) { final VariableTreeItem<String> root = getTreeRoot(); final VariableTreeItem<String> leaf = root.findLeaf( value, contains ); return leaf == null ? root.findLeaf( rtrimTerminalPunctuation( value ) ) : leaf; } /** * Removes punctuation from the end of a string. The character set includes: * <code>:;,.!?-/\¡¿</code>. * * @param s The string to trim, never null. * * @return The string trimmed of all terminal characters from the end */ private String rtrimTerminalPunctuation( final String s ) { final StringBuilder result = new StringBuilder( s.trim() ); while( TERMINALS.contains( "" + result.charAt( result.length() - 1 ) ) ) { result.setLength( result.length() - 1 ); } return result.toString(); } /** * Returns the tree root if either item or its first child are null. * * @param item The item to make null safe. * * @return A non-null TreeItem, possibly the root item (to avoid null). */ private TreeItem<String> sanitize( final TreeItem<String> item ) { TreeItem<String> result; if( item == null ) { result = getTreeRoot(); } else { result = item == getTreeRoot() ? getFirst( item.getChildren() ) : item; } return result; } /** * 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 ) { clearSelection(); selectItem( getTreeView().getRow( item ) ); } private void clearSelection() { getSelectionModel().clearSelection(); } private void selectItem( final int row ) { getSelectionModel().select( row ); } /** * 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 node The nodes to collapse. */ private <T> void collapse( ObservableList<TreeItem<T>> nodes ) { for( final TreeItem<T> node : nodes ) { node.setExpanded( false ); collapse( node.getChildren() ); } } private void initTreeView() { getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE ); } /** * Returns the root node to the tree view. * * @return getTreeView() */ public Node getNode() { return getTreeView(); } private MultipleSelectionModel getSelectionModel() { return getTreeView().getSelectionModel(); } /** * Returns the tree view that contains the YAML definition hierarchy. * * @return A non-null instance. */ private TreeView<String> getTreeView() { return this.treeView; } /** * Returns the root of the tree. * * @return The first node added to the YAML definition tree. */ private VariableTreeItem<String> getTreeRoot() { final TreeItem<String> root = getTreeView().getRoot(); return root instanceof VariableTreeItem ? (VariableTreeItem<String>)root : null; } public <T> boolean isRoot( final TreeItem<T> item ) { return getTreeRoot().equals( item ); } /** * Sets the tree view (called by the constructor). * * @param treeView */ private void setTreeView( final TreeView<String> treeView ) { if( treeView != null ) { this.treeView = treeView; } } }