Skip to content

Commit 48f5293

Browse files
tobiasdiezlenhard
authored andcommitted
Implement #1904: filter groups (#2588)
1 parent 36ccd64 commit 48f5293

11 files changed

+470
-304
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `#
1717
- Redesigned group panel.
1818
- Number of matched entries is always shown.
1919
- The background color of the hit counter signals whether the group contains all/any of the entries selected in the main table.
20+
- Added a possibility to filter the groups panel [#1904](https://github.com/JabRef/jabref/issues/1904)
2021
- Removed edit mode.
2122
- Redesigned about dialog.
2223
- Redesigned key bindings dialog.

src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java

+7-14
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.jabref.model.groups.AllEntriesGroup;
2828
import org.jabref.model.groups.AutomaticGroup;
2929
import org.jabref.model.groups.GroupTreeNode;
30+
import org.jabref.model.strings.StringUtil;
3031

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

137138
GroupNodeViewModel that = (GroupNodeViewModel) o;
138139

139-
if (isRoot != that.isRoot) return false;
140-
if (!displayName.equals(that.displayName)) return false;
141-
if (!iconCode.equals(that.iconCode)) return false;
142-
if (!children.equals(that.children)) return false;
143-
if (!databaseContext.equals(that.databaseContext)) return false;
144140
if (!groupNode.equals(that.groupNode)) return false;
145-
return hits.getValue().equals(that.hits.getValue());
141+
return true;
146142
}
147143

148144
@Override
@@ -160,14 +156,7 @@ public String toString() {
160156

161157
@Override
162158
public int hashCode() {
163-
int result = displayName.hashCode();
164-
result = 31 * result + (isRoot ? 1 : 0);
165-
result = 31 * result + iconCode.hashCode();
166-
result = 31 * result + children.hashCode();
167-
result = 31 * result + databaseContext.hashCode();
168-
result = 31 * result + groupNode.hashCode();
169-
result = 31 * result + hits.hashCode();
170-
return result;
159+
return groupNode.hashCode();
171160
}
172161

173162
public String getIconCode() {
@@ -207,4 +196,8 @@ public GroupTreeNode addSubgroup(AbstractGroup subgroup) {
207196
void toggleExpansion() {
208197
expandedProperty().set(!expandedProperty().get());
209198
}
199+
200+
boolean isMatchedBy(String searchString) {
201+
return StringUtil.isBlank(searchString) || getDisplayName().contains(searchString);
202+
}
210203
}

src/main/java/org/jabref/gui/groups/GroupTree.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
-fx-translate-x: -5px;
108108
}
109109

110-
#buttonBarBottom {
110+
#barBottom {
111111
-fx-background-color: #dadad8;
112112
-fx-border-color: dimgray;
113113
-fx-border-width: 1 0 0 0;

src/main/java/org/jabref/gui/groups/GroupTree.fxml

+18-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
<?import javafx.scene.control.TreeTableColumn?>
77
<?import javafx.scene.control.TreeTableView?>
88
<?import javafx.scene.layout.BorderPane?>
9+
<?import javafx.scene.layout.HBox?>
10+
<?import org.controlsfx.control.textfield.CustomTextField?>
911
<?import org.controlsfx.glyphfont.Glyph?>
1012
<BorderPane xmlns:fx="http://javafx.com/fxml/1" prefHeight="600.0" prefWidth="150.0" styleClass="groupsPane"
1113
xmlns="http://javafx.com/javafx/8.0.112" fx:controller="org.jabref.gui.groups.GroupTreeController">
@@ -24,18 +26,21 @@
2426
</TreeTableView>
2527
</center>
2628
<bottom>
27-
<ButtonBar fx:id="buttonBarBottom">
28-
<buttons>
29-
<Button fx:id="newGroupButton" onAction="#addNewGroup" styleClass="flatButton"
30-
ButtonBar.buttonData="LEFT">
31-
<graphic>
32-
<Glyph fontFamily="FontAwesome" icon="PLUS"/>
33-
</graphic>
34-
<tooltip>
35-
<Tooltip text="%New group"/>
36-
</tooltip>
37-
</Button>
38-
</buttons>
39-
</ButtonBar>
29+
<HBox fx:id="barBottom" alignment="CENTER">
30+
<ButtonBar fx:id="buttonBarBottom">
31+
<buttons>
32+
<Button fx:id="newGroupButton" onAction="#addNewGroup" styleClass="flatButton"
33+
ButtonBar.buttonData="LEFT">
34+
<graphic>
35+
<Glyph fontFamily="FontAwesome" icon="PLUS"/>
36+
</graphic>
37+
<tooltip>
38+
<Tooltip text="%New group"/>
39+
</tooltip>
40+
</Button>
41+
</buttons>
42+
</ButtonBar>
43+
<CustomTextField fx:id="searchField" promptText="Filter groups"/>
44+
</HBox>
4045
</bottom>
4146
</BorderPane>

src/main/java/org/jabref/gui/groups/GroupTreeController.java

+40-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package org.jabref.gui.groups;
22

3+
import java.lang.reflect.InvocationTargetException;
4+
import java.lang.reflect.Method;
5+
36
import javax.inject.Inject;
47

8+
import javafx.beans.property.ObjectProperty;
59
import javafx.css.PseudoClass;
610
import javafx.event.ActionEvent;
711
import javafx.fxml.FXML;
812
import javafx.scene.control.ContextMenu;
913
import javafx.scene.control.Control;
1014
import javafx.scene.control.MenuItem;
1115
import javafx.scene.control.SelectionModel;
16+
import javafx.scene.control.TextField;
1217
import javafx.scene.control.TreeItem;
1318
import javafx.scene.control.TreeTableColumn;
1419
import javafx.scene.control.TreeTableRow;
@@ -24,14 +29,21 @@
2429
import org.jabref.gui.util.ViewModelTreeTableCellFactory;
2530
import org.jabref.logic.l10n.Localization;
2631

32+
import org.apache.commons.logging.Log;
33+
import org.apache.commons.logging.LogFactory;
34+
import org.controlsfx.control.textfield.CustomTextField;
35+
import org.controlsfx.control.textfield.TextFields;
2736
import org.fxmisc.easybind.EasyBind;
2837

2938
public class GroupTreeController extends AbstractController<GroupTreeViewModel> {
3039

40+
private static final Log LOGGER = LogFactory.getLog(GroupTreeController.class);
41+
3142
@FXML private TreeTableView<GroupNodeViewModel> groupTree;
3243
@FXML private TreeTableColumn<GroupNodeViewModel,GroupNodeViewModel> mainColumn;
3344
@FXML private TreeTableColumn<GroupNodeViewModel,GroupNodeViewModel> numberColumn;
3445
@FXML private TreeTableColumn<GroupNodeViewModel,GroupNodeViewModel> disclosureNodeColumn;
46+
@FXML private CustomTextField searchField;
3547

3648
@Inject private StateManager stateManager;
3749
@Inject private DialogService dialogService;
@@ -41,15 +53,23 @@ public void initialize() {
4153
viewModel = new GroupTreeViewModel(stateManager, dialogService);
4254

4355
// Set-up bindings
44-
groupTree.rootProperty().bind(
45-
EasyBind.map(viewModel.rootGroupProperty(),
46-
group -> new RecursiveTreeItem<>(group, GroupNodeViewModel::getChildren, GroupNodeViewModel::expandedProperty))
47-
);
4856
viewModel.selectedGroupProperty().bind(
4957
EasyBind.monadic(groupTree.selectionModelProperty())
5058
.flatMap(SelectionModel::selectedItemProperty)
5159
.selectProperty(TreeItem::valueProperty)
5260
);
61+
viewModel.filterTextProperty().bind(searchField.textProperty());
62+
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
63+
});
64+
65+
groupTree.rootProperty().bind(
66+
EasyBind.map(viewModel.rootGroupProperty(),
67+
group -> new RecursiveTreeItem<>(
68+
group,
69+
GroupNodeViewModel::getChildren,
70+
GroupNodeViewModel::expandedProperty,
71+
viewModel.filterPredicateProperty()))
72+
);
5373

5474
// Icon and group name
5575
mainColumn.setCellValueFactory(cellData -> cellData.getValue().valueProperty());
@@ -122,6 +142,9 @@ public void initialize() {
122142

123143
return row;
124144
});
145+
146+
// Filter text field
147+
setupClearButtonField(searchField);
125148
}
126149

127150
private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) {
@@ -137,4 +160,17 @@ private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) {
137160
public void addNewGroup(ActionEvent actionEvent) {
138161
viewModel.addNewGroupToRoot();
139162
}
163+
164+
/**
165+
* Workaround taken from https://bitbucket.org/controlsfx/controlsfx/issues/330/making-textfieldssetupclearbuttonfield
166+
*/
167+
private void setupClearButtonField(CustomTextField customTextField) {
168+
try {
169+
Method m = TextFields.class.getDeclaredMethod("setupClearButtonField", TextField.class, ObjectProperty.class);
170+
m.setAccessible(true);
171+
m.invoke(null, customTextField, customTextField.rightProperty());
172+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
173+
LOGGER.error("Failed to decorate text field with clear button", ex);
174+
}
175+
}
140176
}

src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java

+20-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import java.util.Objects;
44
import java.util.Optional;
5+
import java.util.function.Predicate;
56

7+
import javafx.beans.binding.Bindings;
68
import javafx.beans.property.ObjectProperty;
79
import javafx.beans.property.SimpleObjectProperty;
10+
import javafx.beans.property.SimpleStringProperty;
11+
import javafx.beans.property.StringProperty;
812

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

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

30-
// Init
31-
onActiveDatabaseChanged(stateManager.activeDatabaseProperty().getValue());
32-
3336
// Register listener
3437
stateManager.activeDatabaseProperty().addListener((observable, oldValue, newValue) -> onActiveDatabaseChanged(newValue));
3538
selectedGroup.addListener((observable, oldValue, newValue) -> onSelectedGroupChanged(newValue));
39+
40+
// Set-up bindings
41+
filterPredicate.bind(Bindings.createObjectBinding(() -> group -> group.isMatchedBy(filterText.get()), filterText));
42+
43+
// Init
44+
onActiveDatabaseChanged(stateManager.activeDatabaseProperty().getValue());
3645
}
3746

3847
public ObjectProperty<GroupNodeViewModel> rootGroupProperty() {
@@ -43,6 +52,14 @@ public ObjectProperty<GroupNodeViewModel> selectedGroupProperty() {
4352
return selectedGroup;
4453
}
4554

55+
public ObjectProperty<Predicate<GroupNodeViewModel>> filterPredicateProperty() {
56+
return filterPredicate;
57+
}
58+
59+
public StringProperty filterTextProperty() {
60+
return filterText;
61+
}
62+
4663
/**
4764
* Gets invoked if the user selects a different group.
4865
* We need to notify the {@link StateManager} about this change so that the main table gets updated.
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package org.jabref.gui.util;
22

33
import java.util.List;
4+
import java.util.function.Predicate;
45
import java.util.stream.Collectors;
56

7+
import javafx.beans.binding.Bindings;
68
import javafx.beans.property.BooleanProperty;
9+
import javafx.beans.property.ObjectProperty;
10+
import javafx.beans.property.SimpleObjectProperty;
11+
import javafx.beans.value.ObservableValue;
712
import javafx.collections.ListChangeListener;
813
import javafx.collections.ObservableList;
14+
import javafx.collections.transformation.FilteredList;
915
import javafx.scene.Node;
1016
import javafx.scene.control.TreeItem;
1117
import javafx.util.Callback;
@@ -17,20 +23,29 @@ public class RecursiveTreeItem<T> extends TreeItem<T> {
1723

1824
private final Callback<T, BooleanProperty> expandedProperty;
1925
private Callback<T, ObservableList<T>> childrenFactory;
26+
private ObjectProperty<Predicate<T>> filter = new SimpleObjectProperty<>();
27+
private FilteredList<T> children;
2028

2129
public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> func) {
22-
this(value, func, null);
30+
this(value, func, null, null);
2331
}
2432

25-
public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> func, Callback<T, BooleanProperty> expandedProperty) {
26-
this(value, (Node) null, func, expandedProperty);
33+
public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> func, Callback<T, BooleanProperty> expandedProperty, ObservableValue<Predicate<T>> filter) {
34+
this(value, null, func, expandedProperty, filter);
2735
}
2836

29-
public RecursiveTreeItem(final T value, Node graphic, Callback<T, ObservableList<T>> func, Callback<T, BooleanProperty> expandedProperty) {
37+
public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> func, ObservableValue<Predicate<T>> filter) {
38+
this(value, null, func, null, filter);
39+
}
40+
41+
private RecursiveTreeItem(final T value, Node graphic, Callback<T, ObservableList<T>> func, Callback<T, BooleanProperty> expandedProperty, ObservableValue<Predicate<T>> filter) {
3042
super(value, graphic);
3143

3244
this.childrenFactory = func;
3345
this.expandedProperty = expandedProperty;
46+
if (filter != null) {
47+
this.filter.bind(filter);
48+
}
3449

3550
if(value != null) {
3651
addChildrenListener(value);
@@ -40,7 +55,7 @@ public RecursiveTreeItem(final T value, Node graphic, Callback<T, ObservableList
4055
valueProperty().addListener((obs, oldValue, newValue)->{
4156
if(newValue != null){
4257
addChildrenListener(newValue);
43-
bindExpandedProperty(value, expandedProperty);
58+
bindExpandedProperty(newValue, expandedProperty);
4459
}
4560
});
4661
}
@@ -52,17 +67,14 @@ private void bindExpandedProperty(T value, Callback<T, BooleanProperty> expanded
5267
}
5368

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

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

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

62-
if(change.wasAdded()){
63-
change.getAddedSubList().forEach(t -> RecursiveTreeItem.this.getChildren().add(new RecursiveTreeItem<>(t, getGraphic(), childrenFactory, expandedProperty)));
64-
}
65-
6678
if(change.wasRemoved()){
6779
change.getRemoved().forEach(t->{
6880
final List<TreeItem<T>> itemsToRemove = RecursiveTreeItem.this.getChildren().stream().filter(treeItem -> treeItem.getValue().equals(t)).collect(Collectors.toList());
@@ -71,7 +83,28 @@ private void addChildrenListener(T value){
7183
});
7284
}
7385

86+
if (change.wasAdded()) {
87+
change.getAddedSubList().forEach(this::addAsChild);
88+
}
7489
}
7590
});
7691
}
92+
93+
private boolean addAsChild(T child) {
94+
return RecursiveTreeItem.this.getChildren().add(new RecursiveTreeItem<>(child, getGraphic(), childrenFactory, expandedProperty, filter));
95+
}
96+
97+
private boolean showNode(T t) {
98+
if (filter.get() == null) {
99+
return true;
100+
}
101+
102+
if (filter.get().test(t)) {
103+
// Node is directly matched -> so show it
104+
return true;
105+
}
106+
107+
// Are there children (or children of children...) that are matched? If yes we also need to show this node
108+
return childrenFactory.call(t).stream().anyMatch(this::showNode);
109+
}
77110
}

0 commit comments

Comments
 (0)