package org.jabref.gui.groups;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.input.Dragboard;
import javafx.scene.paint.Color;
import org.jabref.gui.DragAndDropDataFormats;
import org.jabref.gui.IconTheme;
import org.jabref.gui.StateManager;
import org.jabref.gui.util.BackgroundTask;
import org.jabref.gui.util.BindingsHelper;
import org.jabref.gui.util.TaskExecutor;
import org.jabref.logic.groups.DefaultGroupsFactory;
import org.jabref.logic.layout.format.LatexToUnicodeFormatter;
import org.jabref.model.FieldChange;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.event.EntryEvent;
import org.jabref.model.groups.AbstractGroup;
import org.jabref.model.groups.AutomaticGroup;
import org.jabref.model.groups.GroupEntryChanger;
import org.jabref.model.groups.GroupTreeNode;
import org.jabref.model.strings.StringUtil;
import com.google.common.eventbus.Subscribe;
import org.fxmisc.easybind.EasyBind;
public class GroupNodeViewModel {
private final String displayName;
private final boolean isRoot;
private final ObservableList<GroupNodeViewModel> children;
private final BibDatabaseContext databaseContext;
private final StateManager stateManager;
private final GroupTreeNode groupNode;
private final SimpleIntegerProperty hits;
private final SimpleBooleanProperty hasChildren;
private final SimpleBooleanProperty expandedProperty = new SimpleBooleanProperty();
private final BooleanBinding anySelectedEntriesMatched;
private final BooleanBinding allSelectedEntriesMatched;
private final TaskExecutor taskExecutor;
public GroupNodeViewModel(BibDatabaseContext databaseContext, StateManager stateManager, TaskExecutor taskExecutor, GroupTreeNode groupNode) {
this.databaseContext = Objects.requireNonNull(databaseContext);
this.taskExecutor = Objects.requireNonNull(taskExecutor);
this.stateManager = Objects.requireNonNull(stateManager);
this.groupNode = Objects.requireNonNull(groupNode);
LatexToUnicodeFormatter formatter = new LatexToUnicodeFormatter();
displayName = formatter.format(groupNode.getName());
isRoot = groupNode.isRoot();
if (groupNode.getGroup() instanceof AutomaticGroup) {
AutomaticGroup automaticGroup = (AutomaticGroup) groupNode.getGroup();
children = automaticGroup.createSubgroups(databaseContext.getDatabase().getEntries()).stream()
.map(this::toViewModel)
.sorted((group1, group2) -> group1.getDisplayName().compareToIgnoreCase(group2.getDisplayName()))
.collect(Collectors.toCollection(FXCollections::observableArrayList));
} else {
children = EasyBind.map(groupNode.getChildren(), this::toViewModel);
}
hasChildren = new SimpleBooleanProperty();
hasChildren.bind(Bindings.isNotEmpty(children));
hits = new SimpleIntegerProperty(0);
calculateNumberOfMatches();
expandedProperty.set(groupNode.getGroup().isExpanded());
expandedProperty.addListener((observable, oldValue, newValue) -> groupNode.getGroup().setExpanded(newValue));
// Register listener
databaseContext.getDatabase().registerListener(this);
ObservableList<Boolean> selectedEntriesMatchStatus = EasyBind.map(stateManager.getSelectedEntries(), groupNode::matches);
anySelectedEntriesMatched = BindingsHelper.any(selectedEntriesMatchStatus, matched -> matched);
allSelectedEntriesMatched = BindingsHelper.all(selectedEntriesMatchStatus, matched -> matched);
}
public GroupNodeViewModel(BibDatabaseContext databaseContext, StateManager stateManager, TaskExecutor taskExecutor, AbstractGroup group) {
this(databaseContext, stateManager, taskExecutor, new GroupTreeNode(group));
}
static GroupNodeViewModel getAllEntriesGroup(BibDatabaseContext newDatabase, StateManager stateManager, TaskExecutor taskExecutor) {
return new GroupNodeViewModel(newDatabase, stateManager, taskExecutor, DefaultGroupsFactory.getAllEntriesGroup());
}
private GroupNodeViewModel toViewModel(GroupTreeNode child) {
return new GroupNodeViewModel(databaseContext, stateManager, taskExecutor, child);
}
public List<FieldChange> addEntriesToGroup(List<BibEntry> entries) {
// TODO: warn if assignment has undesired side effects (modifies a field != keywords)
//if (!WarnAssignmentSideEffects.warnAssignmentSideEffects(group, groupSelector.frame))
//{
// return; // user aborted operation
//}
return groupNode.addEntriesToGroup(entries);
// TODO: Store undo
// if (!undo.isEmpty()) {
// groupSelector.concludeAssignment(UndoableChangeEntriesOfGroup.getUndoableEdit(target, undo), target.getNode(), assignedEntries);
}
public SimpleBooleanProperty expandedProperty() {
return expandedProperty;
}
public BooleanBinding anySelectedEntriesMatchedProperty() {
return anySelectedEntriesMatched;
}
public BooleanBinding allSelectedEntriesMatchedProperty() {
return allSelectedEntriesMatched;
}
public SimpleBooleanProperty hasChildrenProperty() {
return hasChildren;
}
public String getDisplayName() {
return displayName;
}
public boolean isRoot() {
return isRoot;
}
public String getDescription() {
return groupNode.getGroup().getDescription().orElse("");
}
public SimpleIntegerProperty getHits() {
return hits;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ((o == null) || (getClass() != o.getClass())) {
return false;
}
GroupNodeViewModel that = (GroupNodeViewModel) o;
if (!groupNode.equals(that.groupNode)) {
return false;
}
return true;
}
@Override
public String toString() {
return "GroupNodeViewModel{" +
"displayName='" + displayName + '\'' +
", isRoot=" + isRoot +
", iconCode='" + getIconCode() + '\'' +
", children=" + children +
", databaseContext=" + databaseContext +
", groupNode=" + groupNode +
", hits=" + hits +
'}';
}
@Override
public int hashCode() {
return groupNode.hashCode();
}
public String getIconCode() {
return groupNode.getGroup().getIconCode().orElse(IconTheme.JabRefIcon.DEFAULT_GROUP_ICON.getCode());
}
public ObservableList<GroupNodeViewModel> getChildren() {
return children;
}
public GroupTreeNode getGroupNode() {
return groupNode;
}
/**
* Gets invoked if an entry in the current database changes.
*/
@Subscribe
public void listen(@SuppressWarnings("unused") EntryEvent entryEvent) {
calculateNumberOfMatches();
}
private void calculateNumberOfMatches() {
// We calculate the new hit value
// We could be more intelligent and try to figure out the new number of hits based on the entry change
// for example, a previously matched entry gets removed -> hits = hits - 1
BackgroundTask
.wrap(() -> groupNode.calculateNumberOfMatches(databaseContext.getDatabase()))
.onSuccess(hits::setValue)
.executeWith(taskExecutor);
}
public GroupTreeNode addSubgroup(AbstractGroup subgroup) {
return groupNode.addSubgroup(subgroup);
}
void toggleExpansion() {
expandedProperty().set(!expandedProperty().get());
}
boolean isMatchedBy(String searchString) {
return StringUtil.isBlank(searchString) || StringUtil.containsIgnoreCase(getDisplayName(), searchString);
}
public Color getColor() {
return groupNode.getGroup().getColor().orElse(IconTheme.getDefaultColor());
}
public String getPath() {
return groupNode.getPath();
}
public Optional<GroupNodeViewModel> getChildByPath(String pathToSource) {
return groupNode.getChildByPath(pathToSource).map(this::toViewModel);
}
/**
* Decides if the content stored in the given {@link Dragboard} can be droped on the given target row.
* Currently, the following sources are allowed:
* - another group (will be added as subgroup on drop)
* - entries if the group implements {@link GroupEntryChanger} (will be assigned to group on drop)
*/
public boolean acceptableDrop(Dragboard dragboard) {
// TODO: we should also check isNodeDescendant
boolean canDropOtherGroup = dragboard.hasContent(DragAndDropDataFormats.GROUP);
boolean canDropEntries = dragboard.hasContent(DragAndDropDataFormats.ENTRIES)
&& groupNode.getGroup() instanceof GroupEntryChanger;
return canDropOtherGroup || canDropEntries;
}
public void moveTo(GroupNodeViewModel target) {
// TODO: Add undo and display message
//MoveGroupChange undo = new MoveGroupChange(((GroupTreeNodeViewModel)source.getParent()).getNode(),
// source.getNode().getPositionInParent(), target.getNode(), target.getChildCount());
getGroupNode().moveTo(target.getGroupNode());
//panel.getUndoManager().addEdit(new UndoableMoveGroup(this.groupsRoot, moveChange));
//panel.markBaseChanged();
//frame.output(Localization.lang("Moved group \"%0\".", node.getNode().getGroup().getName()));
}
}