package org.jabref.gui.groups;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.css.PseudoClass;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import org.jabref.gui.AbstractController;
import org.jabref.gui.DialogService;
import org.jabref.gui.DragAndDropDataFormats;
import org.jabref.gui.StateManager;
import org.jabref.gui.util.BindingsHelper;
import org.jabref.gui.util.RecursiveTreeItem;
import org.jabref.gui.util.TaskExecutor;
import org.jabref.gui.util.ViewModelTreeTableCellFactory;
import org.jabref.logic.l10n.Localization;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.fxmisc.easybind.EasyBind;
public class GroupTreeController extends AbstractController<GroupTreeViewModel> {
private static final Log LOGGER = LogFactory.getLog(GroupTreeController.class);
@FXML private TreeTableView<GroupNodeViewModel> groupTree;
@FXML private TreeTableColumn<GroupNodeViewModel, GroupNodeViewModel> mainColumn;
@FXML private TreeTableColumn<GroupNodeViewModel, GroupNodeViewModel> numberColumn;
@FXML private TreeTableColumn<GroupNodeViewModel, GroupNodeViewModel> disclosureNodeColumn;
@FXML private CustomTextField searchField;
@Inject private StateManager stateManager;
@Inject private DialogService dialogService;
@Inject private TaskExecutor taskExecutor;
@FXML
public void initialize() {
viewModel = new GroupTreeViewModel(stateManager, dialogService, taskExecutor);
// Set-up bindings
groupTree.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> viewModel
.selectedGroupProperty().setValue(newValue != null ? newValue.getValue() : null));
viewModel.selectedGroupProperty().addListener((observable, oldValue, newValue) -> getTreeItemByValue(newValue)
.ifPresent(treeItem -> groupTree.getSelectionModel().select(treeItem)));
viewModel.filterTextProperty().bind(searchField.textProperty());
groupTree.rootProperty().bind(
EasyBind.map(viewModel.rootGroupProperty(),
group -> new RecursiveTreeItem<>(
group,
GroupNodeViewModel::getChildren,
GroupNodeViewModel::expandedProperty,
viewModel.filterPredicateProperty())));
// Icon and group name
mainColumn.setCellValueFactory(cellData -> cellData.getValue().valueProperty());
mainColumn.setCellFactory(new ViewModelTreeTableCellFactory<GroupNodeViewModel, GroupNodeViewModel>()
.withText(GroupNodeViewModel::getDisplayName)
.withIcon(GroupNodeViewModel::getIconCode, GroupNodeViewModel::getColor)
.withTooltip(GroupNodeViewModel::getDescription));
// Number of hits
PseudoClass anySelected = PseudoClass.getPseudoClass("any-selected");
PseudoClass allSelected = PseudoClass.getPseudoClass("all-selected");
numberColumn.setCellFactory(new ViewModelTreeTableCellFactory<GroupNodeViewModel, GroupNodeViewModel>()
.withGraphic(group -> {
final StackPane node = new StackPane();
node.getStyleClass().setAll("hits");
if (!group.isRoot()) {
BindingsHelper.includePseudoClassWhen(node, anySelected,
group.anySelectedEntriesMatchedProperty());
BindingsHelper.includePseudoClassWhen(node, allSelected,
group.allSelectedEntriesMatchedProperty());
}
Text text = new Text();
text.textProperty().bind(group.getHits().asString());
text.getStyleClass().setAll("text");
node.getChildren().add(text);
node.setMaxWidth(Control.USE_PREF_SIZE);
return node;
}));
// Arrow indicating expanded status
disclosureNodeColumn.setCellValueFactory(cellData -> cellData.getValue().valueProperty());
disclosureNodeColumn.setCellFactory(new ViewModelTreeTableCellFactory<GroupNodeViewModel, GroupNodeViewModel>()
.withGraphic(viewModel -> {
final StackPane disclosureNode = new StackPane();
disclosureNode.visibleProperty().bind(viewModel.hasChildrenProperty());
disclosureNode.getStyleClass().setAll("tree-disclosure-node");
final StackPane disclosureNodeArrow = new StackPane();
disclosureNodeArrow.getStyleClass().setAll("arrow");
disclosureNode.getChildren().add(disclosureNodeArrow);
return disclosureNode;
})
.withOnMouseClickedEvent(group -> event -> group.toggleExpansion()));
// Set pseudo-classes to indicate if row is root or sub-item ( > 1 deep)
PseudoClass rootPseudoClass = PseudoClass.getPseudoClass("root");
PseudoClass subElementPseudoClass = PseudoClass.getPseudoClass("sub");
groupTree.setRowFactory(treeTable -> {
TreeTableRow<GroupNodeViewModel> row = new TreeTableRow<>();
row.treeItemProperty().addListener((ov, oldTreeItem, newTreeItem) -> {
boolean isRoot = newTreeItem == treeTable.getRoot();
row.pseudoClassStateChanged(rootPseudoClass, isRoot);
boolean isFirstLevel = (newTreeItem != null) && (newTreeItem.getParent() == treeTable.getRoot());
row.pseudoClassStateChanged(subElementPseudoClass, !isRoot && !isFirstLevel);
});
// Remove disclosure node since we display custom version in separate column
// Simply setting to null is not enough since it would be replaced by the default node on every change
row.setDisclosureNode(null);
row.disclosureNodeProperty().addListener((observable, oldValue, newValue) -> row.setDisclosureNode(null));
// Add context menu (only for non-null items)
row.contextMenuProperty().bind(
EasyBind.monadic(row.itemProperty())
.map(this::createContextMenuForGroup)
.orElse((ContextMenu) null));
// Drag and drop support
row.setOnDragDetected(event -> {
TreeItem<GroupNodeViewModel> selectedItem = treeTable.getSelectionModel().getSelectedItem();
if ((selectedItem != null) && (selectedItem.getValue() != null)) {
Dragboard dragboard = treeTable.startDragAndDrop(TransferMode.MOVE);
// Display the group when dragging
dragboard.setDragView(row.snapshot(null, null));
// Put the group node as content
ClipboardContent content = new ClipboardContent();
content.put(DragAndDropDataFormats.GROUP, selectedItem.getValue().getPath());
dragboard.setContent(content);
event.consume();
}
});
row.setOnDragOver(event -> {
Dragboard dragboard = event.getDragboard();
if ((event.getGestureSource() != row) && row.getItem().acceptableDrop(dragboard)) {
event.acceptTransferModes(TransferMode.MOVE, TransferMode.LINK);
}
event.consume();
});
row.setOnDragDropped(event -> {
Dragboard dragboard = event.getDragboard();
boolean success = false;
if (dragboard.hasContent(DragAndDropDataFormats.GROUP)) {
String pathToSource = (String) dragboard.getContent(DragAndDropDataFormats.GROUP);
Optional<GroupNodeViewModel> source = viewModel.rootGroupProperty().get()
.getChildByPath(pathToSource);
if (source.isPresent()) {
source.get().moveTo(row.getItem());
success = true;
}
}
if (dragboard.hasContent(DragAndDropDataFormats.ENTRIES)) {
TransferableEntrySelection entrySelection = (TransferableEntrySelection) dragboard
.getContent(DragAndDropDataFormats.ENTRIES);
row.getItem().addEntriesToGroup(entrySelection.getSelection());
success = true;
}
event.setDropCompleted(success);
event.consume();
});
return row;
});
// Filter text field
setupClearButtonField(searchField);
}
private Optional<TreeItem<GroupNodeViewModel>> getTreeItemByValue(GroupNodeViewModel value) {
return getTreeItemByValue(groupTree.getRoot(), value);
}
private Optional<TreeItem<GroupNodeViewModel>> getTreeItemByValue(TreeItem<GroupNodeViewModel> root,
GroupNodeViewModel value) {
if (root.getValue().equals(value)) {
return Optional.of(root);
}
for (TreeItem<GroupNodeViewModel> child : root.getChildren()) {
Optional<TreeItem<GroupNodeViewModel>> treeItemByValue = getTreeItemByValue(child, value);
if (treeItemByValue.isPresent()) {
return treeItemByValue;
}
}
return Optional.empty();
}
private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) {
ContextMenu menu = new ContextMenu();
MenuItem editGroup = new MenuItem(Localization.lang("Edit group"));
editGroup.setOnAction(event -> {
menu.hide();
viewModel.editGroup(group);
});
MenuItem addSubgroup = new MenuItem(Localization.lang("Add subgroup"));
addSubgroup.setOnAction(event -> {
menu.hide();
viewModel.addNewSubgroup(group);
});
MenuItem removeGroupAndSubgroups = new MenuItem(Localization.lang("Remove group and subgroups"));
removeGroupAndSubgroups.setOnAction(event -> viewModel.removeGroupAndSubgroups(group));
MenuItem removeGroupKeepSubgroups = new MenuItem(Localization.lang("Remove group, keep subgroups"));
removeGroupKeepSubgroups.setOnAction(event -> viewModel.removeGroupKeepSubgroups(group));
MenuItem removeSubgroups = new MenuItem(Localization.lang("Remove subgroups"));
removeSubgroups.setOnAction(event -> viewModel.removeSubgroups(group));
MenuItem addEntries = new MenuItem(Localization.lang("Add selected entries to this group"));
addEntries.setOnAction(event -> viewModel.addSelectedEntries(group));
MenuItem removeEntries = new MenuItem(Localization.lang("Remove selected entries from this group"));
removeEntries.setOnAction(event -> viewModel.removeSelectedEntries(group));
MenuItem sortAlphabetically = new MenuItem(Localization.lang("Sort all subgroups (recursively)"));
sortAlphabetically.setOnAction(event -> viewModel.sortAlphabeticallyRecursive(group));
menu.getItems().add(editGroup);
menu.getItems().add(new SeparatorMenuItem());
menu.getItems().addAll(addSubgroup, removeSubgroups, removeGroupAndSubgroups, removeGroupKeepSubgroups);
menu.getItems().add(new SeparatorMenuItem());
menu.getItems().addAll(addEntries, removeEntries);
menu.getItems().add(new SeparatorMenuItem());
menu.getItems().add(sortAlphabetically);
return menu;
}
public void addNewGroup(ActionEvent actionEvent) {
viewModel.addNewGroupToRoot();
}
/**
* Workaround taken from https://bitbucket.org/controlsfx/controlsfx/issues/330/making-textfieldssetupclearbuttonfield
*/
private void setupClearButtonField(CustomTextField customTextField) {
try {
Method m = TextFields.class.getDeclaredMethod("setupClearButtonField", TextField.class,
ObjectProperty.class);
m.setAccessible(true);
m.invoke(null, customTextField, customTextField.rightProperty());
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
LOGGER.error("Failed to decorate text field with clear button", ex);
}
}
}