package com.scrivenvar.definition;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
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;
public final class DefinitionPane extends BorderPane {
private final static String TERMINALS = ":;,.!?-/\\¡¿";
private final TreeView<String> mTreeView = new TreeView<>();
private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
= new HashSet<>();
private final StringProperty mFilename = new SimpleStringProperty();
private final TitledPane mTitledPane = new TitledPane();
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;
}
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() );
}
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 );
}
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;
}
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;
}
public VariableTreeItem<String> findLeaf(
final String value, final FindMode findMode ) {
final var root = getTreeRoot();
final var leaf = root.findLeaf( value, findMode );
return leaf == null
? root.findLeaf( rtrimTerminalPunctuation( value ) )
: leaf;
}
private String rtrimTerminalPunctuation( final String s ) {
assert s != null;
int index = s.length() - 1;
while( index > 0 && (TERMINALS.indexOf( s.charAt( index ) ) >= 0) ) {
index--;
}
return s.substring( 0, index );
}
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 ) );
}
public void collapse() {
collapse( getTreeRoot().getChildren() );
}
private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
for( final var node : nodes ) {
node.setExpanded( false );
collapse( node.getChildren() );
}
}
private boolean isEditingTreeItem() {
return getTreeView().editingItemProperty().getValue() != null;
}
private void editSelectedItem() {
getTreeView().edit( getSelectedItem() );
}
private void deleteSelectedItems() {
for( final var item : getSelectedItems() ) {
final var parent = item.getParent();
if( parent != null ) {
parent.getChildren().remove( item );
}
}
}
private void deleteSelectedItem() {
final var c = getSelectedItem();
getSiblings( c ).remove( c );
}
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;
}
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 );
}
}
}
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() ) {
@Override
public void commitEdit( final String newValue ) {
super.commitEdit( newValue );
select( getTreeItem() );
requestFocus();
}
};
}
@Override
public void requestFocus() {
super.requestFocus();
getTreeView().requestFocus();
}
private StringConverter<String> createStringConverter() {
return new StringConverter<>() {
@Override
public String toString( final String object ) {
return object == null ? "" : object;
}
@Override
public String fromString( final String string ) {
return string == null ? "" : string;
}
};
}
public TreeView<String> getTreeView() {
return mTreeView;
}
public Node getNode() {
return this;
}
public StringProperty filenameProperty() {
return mFilename;
}
private VariableTreeItem<String> getTreeRoot() {
final TreeItem<String> root = getTreeView().getRoot();
return root instanceof VariableTreeItem ?
(VariableTreeItem<String>) root : new VariableTreeItem<>( "root" );
}
private ObservableList<TreeItem<String>> getSiblings(
final TreeItem<String> item ) {
final var root = getTreeView().getRoot();
final var parent = (item == null || item == root) ? root : item.getParent();
return parent.getChildren();
}
private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
return getTreeView().getSelectionModel();
}
private List<TreeItem<String>> getSelectedItems() {
return new ArrayList<>( getSelectionModel().getSelectedItems() );
}
public TreeItem<String> getSelectedItem() {
final var item = getSelectionModel().getSelectedItem();
return item == null ? getTreeView().getRoot() : item;
}
private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
return mKeyEventHandlers;
}
}