Skip to content

Commit

Permalink
Implement #1904: filter groups
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiasdiez committed Feb 24, 2017
1 parent 89a95de commit 22c75d1
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 304 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `#
- Redesigned group panel.
- Number of matched entries is always shown.
- The background color of the hit counter signals whether the group contains all/any of the entries selected in the main table.
- Added a possibility to filter the groups panel [#1904](https://github.com/JabRef/jabref/issues/1904)
- Removed edit mode.
- Redesigned about dialog.
- Redesigned key bindings dialog.
Expand Down
21 changes: 7 additions & 14 deletions src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.jabref.model.groups.AllEntriesGroup;
import org.jabref.model.groups.AutomaticGroup;
import org.jabref.model.groups.GroupTreeNode;
import org.jabref.model.strings.StringUtil;

import com.google.common.eventbus.Subscribe;
import org.fxmisc.easybind.EasyBind;
Expand Down Expand Up @@ -136,13 +137,8 @@ public boolean equals(Object o) {

GroupNodeViewModel that = (GroupNodeViewModel) o;

if (isRoot != that.isRoot) return false;
if (!displayName.equals(that.displayName)) return false;
if (!iconCode.equals(that.iconCode)) return false;
if (!children.equals(that.children)) return false;
if (!databaseContext.equals(that.databaseContext)) return false;
if (!groupNode.equals(that.groupNode)) return false;
return hits.getValue().equals(that.hits.getValue());
return true;
}

@Override
Expand All @@ -160,14 +156,7 @@ public String toString() {

@Override
public int hashCode() {
int result = displayName.hashCode();
result = 31 * result + (isRoot ? 1 : 0);
result = 31 * result + iconCode.hashCode();
result = 31 * result + children.hashCode();
result = 31 * result + databaseContext.hashCode();
result = 31 * result + groupNode.hashCode();
result = 31 * result + hits.hashCode();
return result;
return groupNode.hashCode();
}

public String getIconCode() {
Expand Down Expand Up @@ -207,4 +196,8 @@ public GroupTreeNode addSubgroup(AbstractGroup subgroup) {
void toggleExpansion() {
expandedProperty().set(!expandedProperty().get());
}

boolean isMatchedBy(String searchString) {
return StringUtil.isBlank(searchString) || getDisplayName().contains(searchString);
}
}
2 changes: 1 addition & 1 deletion src/main/java/org/jabref/gui/groups/GroupTree.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
-fx-translate-x: -5px;
}

#buttonBarBottom {
#barBottom {
-fx-background-color: #dadad8;
-fx-border-color: dimgray;
-fx-border-width: 1 0 0 0;
Expand Down
31 changes: 18 additions & 13 deletions src/main/java/org/jabref/gui/groups/GroupTree.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<?import javafx.scene.control.TreeTableColumn?>
<?import javafx.scene.control.TreeTableView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import org.controlsfx.control.textfield.CustomTextField?>
<?import org.controlsfx.glyphfont.Glyph?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" prefHeight="600.0" prefWidth="150.0" styleClass="groupsPane"
xmlns="http://javafx.com/javafx/8.0.112" fx:controller="org.jabref.gui.groups.GroupTreeController">
Expand All @@ -24,18 +26,21 @@
</TreeTableView>
</center>
<bottom>
<ButtonBar fx:id="buttonBarBottom">
<buttons>
<Button fx:id="newGroupButton" onAction="#addNewGroup" styleClass="flatButton"
ButtonBar.buttonData="LEFT">
<graphic>
<Glyph fontFamily="FontAwesome" icon="PLUS"/>
</graphic>
<tooltip>
<Tooltip text="%New group"/>
</tooltip>
</Button>
</buttons>
</ButtonBar>
<HBox fx:id="barBottom" alignment="CENTER">
<ButtonBar fx:id="buttonBarBottom">
<buttons>
<Button fx:id="newGroupButton" onAction="#addNewGroup" styleClass="flatButton"
ButtonBar.buttonData="LEFT">
<graphic>
<Glyph fontFamily="FontAwesome" icon="PLUS"/>
</graphic>
<tooltip>
<Tooltip text="%New group"/>
</tooltip>
</Button>
</buttons>
</ButtonBar>
<CustomTextField fx:id="searchField" promptText="Filter groups"/>
</HBox>
</bottom>
</BorderPane>
44 changes: 40 additions & 4 deletions src/main/java/org/jabref/gui/groups/GroupTreeController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package org.jabref.gui.groups;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

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.SelectionModel;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
Expand All @@ -24,14 +29,21 @@
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;
Expand All @@ -41,15 +53,23 @@ public void initialize() {
viewModel = new GroupTreeViewModel(stateManager, dialogService);

// Set-up bindings
groupTree.rootProperty().bind(
EasyBind.map(viewModel.rootGroupProperty(),
group -> new RecursiveTreeItem<>(group, GroupNodeViewModel::getChildren, GroupNodeViewModel::expandedProperty))
);
viewModel.selectedGroupProperty().bind(
EasyBind.monadic(groupTree.selectionModelProperty())
.flatMap(SelectionModel::selectedItemProperty)
.selectProperty(TreeItem::valueProperty)
);
viewModel.filterTextProperty().bind(searchField.textProperty());
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
});

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());
Expand Down Expand Up @@ -122,6 +142,9 @@ public void initialize() {

return row;
});

// Filter text field
setupClearButtonField(searchField);
}

private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) {
Expand All @@ -141,4 +164,17 @@ private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) {
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);
}
}
}
23 changes: 20 additions & 3 deletions src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;

import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

import org.jabref.gui.AbstractViewModel;
import org.jabref.gui.DialogService;
Expand All @@ -21,18 +25,23 @@ public class GroupTreeViewModel extends AbstractViewModel {
private final ObjectProperty<GroupNodeViewModel> selectedGroup = new SimpleObjectProperty<>();
private final StateManager stateManager;
private final DialogService dialogService;
private final ObjectProperty<Predicate<GroupNodeViewModel>> filterPredicate = new SimpleObjectProperty<>();
private final StringProperty filterText = new SimpleStringProperty();
private Optional<BibDatabaseContext> currentDatabase;

public GroupTreeViewModel(StateManager stateManager, DialogService dialogService) {
this.stateManager = Objects.requireNonNull(stateManager);
this.dialogService = Objects.requireNonNull(dialogService);

// Init
onActiveDatabaseChanged(stateManager.activeDatabaseProperty().getValue());

// Register listener
stateManager.activeDatabaseProperty().addListener((observable, oldValue, newValue) -> onActiveDatabaseChanged(newValue));
selectedGroup.addListener((observable, oldValue, newValue) -> onSelectedGroupChanged(newValue));

// Set-up bindings
filterPredicate.bind(Bindings.createObjectBinding(() -> group -> group.isMatchedBy(filterText.get()), filterText));

// Init
onActiveDatabaseChanged(stateManager.activeDatabaseProperty().getValue());
}

public ObjectProperty<GroupNodeViewModel> rootGroupProperty() {
Expand All @@ -43,6 +52,14 @@ public ObjectProperty<GroupNodeViewModel> selectedGroupProperty() {
return selectedGroup;
}

public ObjectProperty<Predicate<GroupNodeViewModel>> filterPredicateProperty() {
return filterPredicate;
}

public StringProperty filterTextProperty() {
return filterText;
}

/**
* Gets invoked if the user selects a different group.
* We need to notify the {@link StateManager} about this change so that the main table gets updated.
Expand Down
55 changes: 44 additions & 11 deletions src/main/java/org/jabref/gui/util/RecursiveTreeItem.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package org.jabref.gui.util;

import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Node;
import javafx.scene.control.TreeItem;
import javafx.util.Callback;
Expand All @@ -17,20 +23,29 @@ public class RecursiveTreeItem<T> extends TreeItem<T> {

private final Callback<T, BooleanProperty> expandedProperty;
private Callback<T, ObservableList<T>> childrenFactory;
private ObjectProperty<Predicate<T>> filter = new SimpleObjectProperty<>();
private FilteredList<T> children;

public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> func) {
this(value, func, null);
this(value, func, null, null);
}

public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> func, Callback<T, BooleanProperty> expandedProperty) {
this(value, (Node) null, func, expandedProperty);
public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> func, Callback<T, BooleanProperty> expandedProperty, ObservableValue<Predicate<T>> filter) {
this(value, null, func, expandedProperty, filter);
}

public RecursiveTreeItem(final T value, Node graphic, Callback<T, ObservableList<T>> func, Callback<T, BooleanProperty> expandedProperty) {
public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> func, ObservableValue<Predicate<T>> filter) {
this(value, null, func, null, filter);
}

private RecursiveTreeItem(final T value, Node graphic, Callback<T, ObservableList<T>> func, Callback<T, BooleanProperty> expandedProperty, ObservableValue<Predicate<T>> filter) {
super(value, graphic);

this.childrenFactory = func;
this.expandedProperty = expandedProperty;
if (filter != null) {
this.filter.bind(filter);
}

if(value != null) {
addChildrenListener(value);
Expand All @@ -40,7 +55,7 @@ public RecursiveTreeItem(final T value, Node graphic, Callback<T, ObservableList
valueProperty().addListener((obs, oldValue, newValue)->{
if(newValue != null){
addChildrenListener(newValue);
bindExpandedProperty(value, expandedProperty);
bindExpandedProperty(newValue, expandedProperty);
}
});
}
Expand All @@ -52,17 +67,14 @@ private void bindExpandedProperty(T value, Callback<T, BooleanProperty> expanded
}

private void addChildrenListener(T value){
final ObservableList<T> children = childrenFactory.call(value);
children = new FilteredList<>(childrenFactory.call(value));
children.predicateProperty().bind(Bindings.createObjectBinding(() -> this::showNode, filter));

children.forEach(child -> RecursiveTreeItem.this.getChildren().add(new RecursiveTreeItem<>(child, getGraphic(), childrenFactory, expandedProperty)));
children.forEach(this::addAsChild);

children.addListener((ListChangeListener<T>) change -> {
while(change.next()){

if(change.wasAdded()){
change.getAddedSubList().forEach(t -> RecursiveTreeItem.this.getChildren().add(new RecursiveTreeItem<>(t, getGraphic(), childrenFactory, expandedProperty)));
}

if(change.wasRemoved()){
change.getRemoved().forEach(t->{
final List<TreeItem<T>> itemsToRemove = RecursiveTreeItem.this.getChildren().stream().filter(treeItem -> treeItem.getValue().equals(t)).collect(Collectors.toList());
Expand All @@ -71,7 +83,28 @@ private void addChildrenListener(T value){
});
}

if (change.wasAdded()) {
change.getAddedSubList().forEach(this::addAsChild);
}
}
});
}

private boolean addAsChild(T child) {
return RecursiveTreeItem.this.getChildren().add(new RecursiveTreeItem<>(child, getGraphic(), childrenFactory, expandedProperty, filter));
}

private boolean showNode(T t) {
if (filter.get() == null) {
return true;
}

if (filter.get().test(t)) {
// Node is directly matched -> so show it
return true;
}

// Are there children (or children of children...) that are matched? If yes we also need to show this node
return childrenFactory.call(t).stream().anyMatch(this::showNode);
}
}
Loading

0 comments on commit 22c75d1

Please sign in to comment.