Skip to content

Commit 9ca639d

Browse files
committed
Implement #2786: Allow selection of multiple groups
1 parent 2cbfbc5 commit 9ca639d

7 files changed

+115
-62
lines changed

CHANGELOG.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `#
1111
## [Unreleased]
1212

1313
### Changed
14-
- Continued to redesign the user interface: this time the editor got a fresh coat of paint:
14+
- We continued to improve the new groups interface:
15+
- You can now again select multiple groups (and a few related settings were added to the preferences) [#2786](https://github.com/JabRef/jabref/issues/2786).
16+
- The entry editor got a fresh coat of paint:
17+
- Homogenize the size of text fields.
1518
- The buttons were changed to icons.
19+
- Completely new interface to add or modify linked files.
1620
- Removed the hidden feature that a double click in the editor inserted the current date.
1721
- All authors and editors are separated using semicolons when exporting to csv. [#2762](https://github.com/JabRef/jabref/issues/2762)
18-
- Improved wording of "Show recommendationns: into "Show 'Related Articles' tab" in the preferences
22+
- Improved wording of "Show recommendations: into "Show 'Related Articles' tab" in the preferences
1923

2024
### Fixed
2125
- We fixed the IEEE Xplore web search functionality [#2789](https://github.com/JabRef/jabref/issues/2789)

src/main/java/org/jabref/gui/BasePanel.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -2151,7 +2151,7 @@ public void listen(EntryAddedEvent addedEntryEvent) {
21512151
if (Globals.prefs.getBoolean(JabRefPreferences.AUTO_ASSIGN_GROUP)
21522152
&& frame.getGroupSelector().getToggleAction().isSelected()) {
21532153
final List<BibEntry> entries = Collections.singletonList(addedEntryEvent.getBibEntry());
2154-
Globals.stateManager.getSelectedGroup(bibDatabaseContext).ifPresent(
2154+
Globals.stateManager.getSelectedGroup(bibDatabaseContext).forEach(
21552155
selectedGroup -> selectedGroup.addEntriesToGroup(entries));
21562156
SwingUtilities.invokeLater(() -> BasePanel.this.getGroupSelector().valueChanged(null));
21572157
}

src/main/java/org/jabref/gui/StateManager.java

+14-13
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
import javafx.beans.binding.Bindings;
99
import javafx.beans.property.ObjectProperty;
10-
import javafx.beans.property.ReadOnlyObjectProperty;
11-
import javafx.beans.property.ReadOnlyObjectWrapper;
10+
import javafx.beans.property.ReadOnlyListProperty;
11+
import javafx.beans.property.ReadOnlyListWrapper;
1212
import javafx.beans.property.SimpleObjectProperty;
1313
import javafx.collections.FXCollections;
1414
import javafx.collections.ObservableList;
@@ -33,21 +33,21 @@
3333
public class StateManager {
3434

3535
private final ObjectProperty<Optional<BibDatabaseContext>> activeDatabase = new SimpleObjectProperty<>(Optional.empty());
36-
private final ReadOnlyObjectWrapper<Optional<GroupTreeNode>> activeGroup = new ReadOnlyObjectWrapper<>(Optional.empty());
36+
private final ReadOnlyListWrapper<GroupTreeNode> activeGroups = new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
3737
private final ObservableList<BibEntry> selectedEntries = FXCollections.observableArrayList();
38-
private final ObservableMap<BibDatabaseContext, GroupTreeNode> selectedGroups = FXCollections.observableHashMap();
38+
private final ObservableMap<BibDatabaseContext, ObservableList<GroupTreeNode>> selectedGroups = FXCollections.observableHashMap();
3939

4040
public StateManager() {
4141
MonadicBinding<BibDatabaseContext> currentDatabase = EasyBind.map(activeDatabase, database -> database.orElse(null));
42-
activeGroup.bind(EasyBind.map(Bindings.valueAt(selectedGroups, currentDatabase), Optional::ofNullable));
42+
activeGroups.bind(Bindings.valueAt(selectedGroups, currentDatabase));
4343
}
4444

4545
public ObjectProperty<Optional<BibDatabaseContext>> activeDatabaseProperty() {
4646
return activeDatabase;
4747
}
4848

49-
public ReadOnlyObjectProperty<Optional<GroupTreeNode>> activeGroupProperty() {
50-
return activeGroup.getReadOnlyProperty();
49+
public ReadOnlyListProperty<GroupTreeNode> activeGroupProperty() {
50+
return activeGroups.getReadOnlyProperty();
5151
}
5252

5353
public ObservableList<BibEntry> getSelectedEntries() {
@@ -58,16 +58,17 @@ public void setSelectedEntries(List<BibEntry> newSelectedEntries) {
5858
selectedEntries.setAll(newSelectedEntries);
5959
}
6060

61-
public void setSelectedGroup(BibDatabaseContext database, GroupTreeNode newSelectedGroup) {
62-
Objects.requireNonNull(newSelectedGroup);
63-
selectedGroups.put(database, newSelectedGroup);
61+
public void setSelectedGroups(BibDatabaseContext database, List<GroupTreeNode> newSelectedGroups) {
62+
Objects.requireNonNull(newSelectedGroups);
63+
selectedGroups.put(database, FXCollections.observableArrayList(newSelectedGroups));
6464
}
6565

66-
public Optional<GroupTreeNode> getSelectedGroup(BibDatabaseContext database) {
67-
return Optional.ofNullable(selectedGroups.get(database));
66+
public ObservableList<GroupTreeNode> getSelectedGroup(BibDatabaseContext database) {
67+
ObservableList<GroupTreeNode> selectedGroupsForDatabase = selectedGroups.get(database);
68+
return selectedGroupsForDatabase != null ? selectedGroupsForDatabase : FXCollections.observableArrayList();
6869
}
6970

70-
public void clearSelectedGroup(BibDatabaseContext database) {
71+
public void clearSelectedGroups(BibDatabaseContext database) {
7172
selectedGroups.remove(database);
7273
}
7374

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

+13-17
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.awt.event.MouseEvent;
1010
import java.util.Enumeration;
1111
import java.util.List;
12-
import java.util.Optional;
1312

1413
import javax.swing.AbstractAction;
1514
import javax.swing.BorderFactory;
@@ -55,6 +54,8 @@
5554
import org.jabref.model.groups.event.GroupUpdatedEvent;
5655
import org.jabref.model.metadata.MetaData;
5756
import org.jabref.model.search.SearchMatcher;
57+
import org.jabref.model.search.matchers.MatcherSet;
58+
import org.jabref.model.search.matchers.MatcherSets;
5859
import org.jabref.preferences.JabRefPreferences;
5960

6061
import com.google.common.eventbus.Subscribe;
@@ -329,27 +330,22 @@ public void valueChanged(TreeSelectionEvent e) {
329330

330331
private void updateShownEntriesAccordingToSelectedGroups() {
331332
updateShownEntriesAccordingToSelectedGroups(Globals.stateManager.activeGroupProperty().get());
332-
/*final MatcherSet searchRules = MatcherSets
333-
.build(andCb.isSelected() ? MatcherSets.MatcherType.AND : MatcherSets.MatcherType.OR);
334-
335-
for (GroupTreeNodeViewModel node : getLeafsOfSelection()) {
336-
SearchMatcher searchRule = node.getNode().getSearchMatcher();
337-
searchRules.addRule(searchRule);
338-
}
339-
SearchMatcher searchRule = invCb.isSelected() ? new NotMatcher(searchRules) : searchRules;
340-
GroupingWorker worker = new GroupingWorker(searchRule);
341-
worker.getWorker().run();
342-
worker.getCallBack().update();
343-
*/
344333
}
345334

346-
private void updateShownEntriesAccordingToSelectedGroups(Optional<GroupTreeNode> selectedGroup) {
347-
if (!selectedGroup.isPresent()) {
335+
private void updateShownEntriesAccordingToSelectedGroups(List<GroupTreeNode> selectedGroups) {
336+
if (selectedGroups == null || selectedGroups.isEmpty()) {
348337
// No selected group, nothing to do
349338
return;
350339
}
351-
SearchMatcher searchRule = selectedGroup.get().getSearchMatcher();
352-
GroupingWorker worker = new GroupingWorker(searchRule);
340+
341+
final MatcherSet searchRules = MatcherSets.build(
342+
Globals.prefs.getBoolean(JabRefPreferences.GROUP_INTERSECT_SELECTIONS) ? MatcherSets.MatcherType.AND : MatcherSets.MatcherType.OR);
343+
344+
for (GroupTreeNode node : selectedGroups) {
345+
searchRules.addRule(node.getSearchMatcher());
346+
}
347+
348+
GroupingWorker worker = new GroupingWorker(searchRules);
353349
worker.run();
354350
worker.update();
355351
}

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

+30-4
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
import java.lang.reflect.InvocationTargetException;
44
import java.lang.reflect.Method;
5+
import java.util.List;
56
import java.util.Optional;
7+
import java.util.function.Consumer;
8+
import java.util.stream.Collectors;
69

710
import javax.inject.Inject;
811

912
import javafx.beans.property.ObjectProperty;
13+
import javafx.collections.ObservableList;
1014
import javafx.css.PseudoClass;
1115
import javafx.event.ActionEvent;
1216
import javafx.fxml.FXML;
1317
import javafx.scene.control.ContextMenu;
1418
import javafx.scene.control.Control;
1519
import javafx.scene.control.MenuItem;
20+
import javafx.scene.control.SelectionMode;
1621
import javafx.scene.control.SeparatorMenuItem;
1722
import javafx.scene.control.TextField;
1823
import javafx.scene.control.TreeItem;
@@ -59,11 +64,27 @@ public class GroupTreeController extends AbstractController<GroupTreeViewModel>
5964
public void initialize() {
6065
viewModel = new GroupTreeViewModel(stateManager, dialogService, taskExecutor);
6166

67+
// Set-up groups tree
68+
groupTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
69+
6270
// Set-up bindings
63-
groupTree.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> viewModel
64-
.selectedGroupProperty().setValue(newValue != null ? newValue.getValue() : null));
65-
viewModel.selectedGroupProperty().addListener((observable, oldValue, newValue) -> getTreeItemByValue(newValue)
66-
.ifPresent(treeItem -> groupTree.getSelectionModel().select(treeItem)));
71+
Consumer<ObservableList<GroupNodeViewModel>> updateSelectedGroups =
72+
(newSelectedGroups) -> newSelectedGroups.forEach(this::selectNode);
73+
Consumer<List<TreeItem<GroupNodeViewModel>>> updateViewModel =
74+
(newSelectedGroups) -> {
75+
if (newSelectedGroups == null) {
76+
viewModel.selectedGroupsProperty().clear();
77+
} else {
78+
viewModel.selectedGroupsProperty().setAll(newSelectedGroups.stream().map(TreeItem::getValue).collect(Collectors.toList()));
79+
}
80+
};
81+
BindingsHelper.bindContentBidirectional(
82+
groupTree.getSelectionModel().getSelectedItems(),
83+
viewModel.selectedGroupsProperty(),
84+
updateSelectedGroups,
85+
updateViewModel
86+
);
87+
6788
viewModel.filterTextProperty().bind(searchField.textProperty());
6889

6990
groupTree.rootProperty().bind(
@@ -196,6 +217,11 @@ public void initialize() {
196217
setupClearButtonField(searchField);
197218
}
198219

220+
private void selectNode(GroupNodeViewModel value) {
221+
getTreeItemByValue(value)
222+
.ifPresent(treeItem -> groupTree.getSelectionModel().select(treeItem));
223+
}
224+
199225
private Optional<TreeItem<GroupNodeViewModel>> getTreeItemByValue(GroupNodeViewModel value) {
200226
return getTreeItemByValue(groupTree.getRoot(), value);
201227
}

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

+17-11
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
import java.util.Objects;
66
import java.util.Optional;
77
import java.util.function.Predicate;
8+
import java.util.stream.Collectors;
89

910
import javax.swing.SwingUtilities;
1011

1112
import javafx.application.Platform;
1213
import javafx.beans.binding.Bindings;
14+
import javafx.beans.property.ListProperty;
1315
import javafx.beans.property.ObjectProperty;
16+
import javafx.beans.property.SimpleListProperty;
1417
import javafx.beans.property.SimpleObjectProperty;
1518
import javafx.beans.property.SimpleStringProperty;
1619
import javafx.beans.property.StringProperty;
20+
import javafx.collections.FXCollections;
21+
import javafx.collections.ObservableList;
1722

1823
import org.jabref.gui.AbstractViewModel;
1924
import org.jabref.gui.DialogService;
@@ -30,16 +35,16 @@
3035
public class GroupTreeViewModel extends AbstractViewModel {
3136

3237
private final ObjectProperty<GroupNodeViewModel> rootGroup = new SimpleObjectProperty<>();
33-
private final ObjectProperty<GroupNodeViewModel> selectedGroup = new SimpleObjectProperty<>();
38+
private final ListProperty<GroupNodeViewModel> selectedGroups = new SimpleListProperty<>(FXCollections.observableArrayList());
3439
private final StateManager stateManager;
3540
private final DialogService dialogService;
3641
private final TaskExecutor taskExecutor;
3742
private final ObjectProperty<Predicate<GroupNodeViewModel>> filterPredicate = new SimpleObjectProperty<>();
3843
private final StringProperty filterText = new SimpleStringProperty();
39-
private Optional<BibDatabaseContext> currentDatabase;
4044
private final Comparator<GroupTreeNode> compAlphabetIgnoreCase = (GroupTreeNode v1, GroupTreeNode v2) -> v1
4145
.getName()
4246
.compareToIgnoreCase(v2.getName());
47+
private Optional<BibDatabaseContext> currentDatabase;
4348

4449
public GroupTreeViewModel(StateManager stateManager, DialogService dialogService, TaskExecutor taskExecutor) {
4550
this.stateManager = Objects.requireNonNull(stateManager);
@@ -49,7 +54,7 @@ public GroupTreeViewModel(StateManager stateManager, DialogService dialogService
4954
// Register listener
5055
stateManager.activeDatabaseProperty()
5156
.addListener((observable, oldValue, newValue) -> onActiveDatabaseChanged(newValue));
52-
selectedGroup.addListener((observable, oldValue, newValue) -> onSelectedGroupChanged(newValue));
57+
selectedGroups.addListener((observable, oldValue, newValue) -> onSelectedGroupChanged(newValue));
5358

5459
// Set-up bindings
5560
filterPredicate
@@ -63,8 +68,8 @@ public ObjectProperty<GroupNodeViewModel> rootGroupProperty() {
6368
return rootGroup;
6469
}
6570

66-
public ObjectProperty<GroupNodeViewModel> selectedGroupProperty() {
67-
return selectedGroup;
71+
public ListProperty<GroupNodeViewModel> selectedGroupsProperty() {
72+
return selectedGroups;
6873
}
6974

7075
public ObjectProperty<Predicate<GroupNodeViewModel>> filterPredicateProperty() {
@@ -79,17 +84,17 @@ public StringProperty filterTextProperty() {
7984
* Gets invoked if the user selects a different group.
8085
* We need to notify the {@link StateManager} about this change so that the main table gets updated.
8186
*/
82-
private void onSelectedGroupChanged(GroupNodeViewModel newValue) {
87+
private void onSelectedGroupChanged(ObservableList<GroupNodeViewModel> newValue) {
8388
if (!currentDatabase.equals(stateManager.activeDatabaseProperty().getValue())) {
8489
// Switch of database occurred -> do nothing
8590
return;
8691
}
8792

8893
currentDatabase.ifPresent(database -> {
8994
if (newValue == null) {
90-
stateManager.clearSelectedGroup(database);
95+
stateManager.clearSelectedGroups(database);
9196
} else {
92-
stateManager.setSelectedGroup(database, newValue.getGroupNode());
97+
stateManager.setSelectedGroups(database, newValue.stream().map(GroupNodeViewModel::getGroupNode).collect(Collectors.toList()));
9398
}
9499
});
95100
}
@@ -114,9 +119,10 @@ private void onActiveDatabaseChanged(Optional<BibDatabaseContext> newDatabase) {
114119
.orElse(GroupNodeViewModel.getAllEntriesGroup(newDatabase.get(), stateManager, taskExecutor));
115120

116121
rootGroup.setValue(newRoot);
117-
stateManager.getSelectedGroup(newDatabase.get()).ifPresent(
118-
selectedGroup -> this.selectedGroup.setValue(
119-
new GroupNodeViewModel(newDatabase.get(), stateManager, taskExecutor, selectedGroup)));
122+
this.selectedGroups.setAll(
123+
stateManager.getSelectedGroup(newDatabase.get()).stream()
124+
.map(selectedGroup -> new GroupNodeViewModel(newDatabase.get(), stateManager, taskExecutor, selectedGroup))
125+
.collect(Collectors.toList()));
120126
}
121127

122128
currentDatabase = newDatabase;

src/main/java/org/jabref/gui/util/BindingsHelper.java

+34-14
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import javafx.css.PseudoClass;
1919
import javafx.scene.Node;
2020

21+
2122
/**
2223
* Helper methods for javafx binding.
2324
* Some methods are taken from https://bugs.openjdk.java.net/browse/JDK-8134679
@@ -86,14 +87,33 @@ public static <A, B> void bindBidirectional(ObservableValue<A> propertyA, Observ
8687
propertyB.addListener(binding.getChangeListenerB());
8788
}
8889

89-
public static <A, B> void bindContentBidirectional(ListProperty<A> listProperty, Property<B> property, Function<List<A>, B> mapToB, Function<B, List<A>> mapToList) {
90-
final BidirectionalListBinding<A, B> binding = new BidirectionalListBinding<>(listProperty, property, mapToB, mapToList);
90+
public static <A, B> void bindContentBidirectional(ObservableList<A> propertyA, ListProperty<B> propertyB, Consumer<ObservableList<B>> updateA, Consumer<List<A>> updateB) {
91+
bindContentBidirectional(
92+
propertyA,
93+
(ObservableValue<ObservableList<B>>) propertyB,
94+
updateA,
95+
updateB);
96+
}
97+
98+
public static <A, B> void bindContentBidirectional(ObservableList<A> propertyA, ObservableValue<B> propertyB, Consumer<B> updateA, Consumer<List<A>> updateB) {
99+
final BidirectionalListBinding<A, B> binding = new BidirectionalListBinding<>(propertyA, propertyB, updateA, updateB);
91100

92101
// use property as initial source
93-
listProperty.setAll(mapToList.apply(property.getValue()));
102+
updateA.accept(propertyB.getValue());
94103

95-
listProperty.addListener(binding);
96-
property.addListener(binding);
104+
propertyA.addListener(binding);
105+
propertyB.addListener(binding);
106+
}
107+
108+
public static <A, B> void bindContentBidirectional(ListProperty<A> listProperty, Property<B> property, Function<List<A>, B> mapToB, Function<B, List<A>> mapToList) {
109+
Consumer<B> updateList = newValueB -> listProperty.setAll(mapToList.apply(newValueB));
110+
Consumer<List<A>> updateB = newValueList -> property.setValue(mapToB.apply(newValueList));
111+
112+
bindContentBidirectional(
113+
listProperty,
114+
property,
115+
updateList,
116+
updateB);
97117
}
98118

99119
private static class BidirectionalBinding<A, B> {
@@ -139,25 +159,25 @@ private <T> void updateLocked(Consumer<T> update, T oldValue, T newValue) {
139159

140160
private static class BidirectionalListBinding<A, B> implements ListChangeListener<A>, ChangeListener<B> {
141161

142-
private final ListProperty<A> listProperty;
143-
private final Property<B> property;
144-
private final Function<List<A>, B> mapToB;
145-
private final Function<B, List<A>> mapToList;
162+
private final ObservableList<A> listProperty;
163+
private final ObservableValue<B> property;
164+
private final Consumer<B> updateA;
165+
private final Consumer<List<A>> updateB;
146166
private boolean updating = false;
147167

148-
public BidirectionalListBinding(ListProperty<A> listProperty, Property<B> property, Function<List<A>, B> mapToB, Function<B, List<A>> mapToList) {
168+
public BidirectionalListBinding(ObservableList<A> listProperty, ObservableValue<B> property, Consumer<B> updateA, Consumer<List<A>> updateB) {
149169
this.listProperty = listProperty;
150170
this.property = property;
151-
this.mapToB = mapToB;
152-
this.mapToList = mapToList;
171+
this.updateA = updateA;
172+
this.updateB = updateB;
153173
}
154174

155175
@Override
156176
public void changed(ObservableValue<? extends B> observable, B oldValue, B newValue) {
157177
if (!updating) {
158178
try {
159179
updating = true;
160-
listProperty.setAll(mapToList.apply(newValue));
180+
updateA.accept(newValue);
161181
} finally {
162182
updating = false;
163183
}
@@ -169,7 +189,7 @@ public void onChanged(Change<? extends A> c) {
169189
if (!updating) {
170190
try {
171191
updating = true;
172-
property.setValue(mapToB.apply(listProperty.getValue()));
192+
updateB.accept(listProperty);
173193
} finally {
174194
updating = false;
175195
}

0 commit comments

Comments
 (0)