Skip to content

Commit

Permalink
Implemented TagsField for the Keywords field (#10910)
Browse files Browse the repository at this point in the history
* added selection box next to the keyword field

* added selection box next to the keyword field

* Update CHANGELOG.md

* Rewrite keywords field: used tags field

* Update src/main/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModel.java

Co-authored-by: Oliver Kopp <kopp.dev@gmail.com>

* Fix Suggestion Provider [#8145]

* Add context menu for keywordsEditor

* Fixed OpenRewrite test issue

* Add typed text as first suggestion

* Revert "Fix Suggestion Provider"

* Remove unused imports

* Bind the keywords and tags field

* Update CHANGELOG.md

* Remove duplicates from the suggestions list

* use Labels as a tag view

* Add LOGGER.debug

* remove chip-view from CSS file

* Update CHANGELOG.md

---------

Co-authored-by: Oliver Kopp <kopp.dev@gmail.com>
  • Loading branch information
LoayGhreeb and koppor authored Mar 5, 2024
1 parent f1c099c commit 8374693
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We made the command "Push to TexShop" more robust to allow cite commands with a character before the first slash. [forum#2699](https://discourse.jabref.org/t/push-to-texshop-mac/2699/17?u=siedlerchr)
- We only show the notification "Saving library..." if the library contains more than 2000 entries. [#9803](https://github.com/JabRef/jabref/issues/9803)
- We enhanced the dialog for adding new fields in the content selector with a selection box containing a list of standard fields. [#10912](https://github.com/JabRef/jabref/pull/10912)
- Keywords filed are now displayed as tags. [#10910](https://github.com/JabRef/jabref/pull/10910)

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public static FieldEditorFX getForField(final Field field,
} else if (fieldProperties.contains(FieldProperty.PERSON_NAMES)) {
return new PersonsEditor(field, suggestionProvider, preferences, fieldCheckers, isMultiLine, undoManager);
} else if (StandardField.KEYWORDS == field) {
return new KeywordsEditor(field, suggestionProvider, fieldCheckers, preferences, undoManager);
return new KeywordsEditor(field, suggestionProvider, fieldCheckers);
} else if (field == InternalField.KEY_FIELD) {
return new CitationKeyEditor(field, suggestionProvider, fieldCheckers, databaseContext);
} else if (fieldProperties.contains(FieldProperty.MARKDOWN)) {
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.fxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.HBox?>
<?import com.dlsc.gemsfx.TagsField?>
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="HBox" xmlns="http://javafx.com/javafx/8.0.112"
fx:controller="org.jabref.gui.fieldeditors.KeywordsEditor" >
<TagsField fx:id="keywordTagsField" HBox.hgrow="ALWAYS"/>
</fx:root>
141 changes: 136 additions & 5 deletions src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,155 @@
package org.jabref.gui.fieldeditors;

import java.util.Comparator;

import javax.swing.undo.UndoManager;

import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;

import org.jabref.gui.ClipBoardManager;
import org.jabref.gui.DialogService;
import org.jabref.gui.JabRefDialogService;
import org.jabref.gui.actions.ActionFactory;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.actions.StandardActions;
import org.jabref.gui.autocompleter.SuggestionProvider;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.keyboard.KeyBindingRepository;
import org.jabref.gui.util.ViewModelListCellFactory;
import org.jabref.logic.integrity.FieldCheckers;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.Keyword;
import org.jabref.model.entry.field.Field;
import org.jabref.preferences.PreferencesService;

public class KeywordsEditor extends SimpleEditor implements FieldEditorFX {
import com.airhacks.afterburner.views.ViewLoader;
import com.dlsc.gemsfx.TagsField;
import jakarta.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class KeywordsEditor extends HBox implements FieldEditorFX {
private static final Logger LOGGER = LoggerFactory.getLogger(KeywordsEditor.class);

@FXML private TagsField<Keyword> keywordTagsField;

@Inject private PreferencesService preferencesService;
@Inject private DialogService dialogService;
@Inject private UndoManager undoManager;
@Inject private ClipBoardManager clipBoardManager;
@Inject private KeyBindingRepository keyBindingRepository;

private final KeywordsEditorViewModel viewModel;

public KeywordsEditor(Field field,
SuggestionProvider<?> suggestionProvider,
FieldCheckers fieldCheckers,
PreferencesService preferences,
UndoManager undoManager) {
super(field, suggestionProvider, fieldCheckers, preferences, undoManager);
FieldCheckers fieldCheckers) {

ViewLoader.view(this)
.root(this)
.load();

this.viewModel = new KeywordsEditorViewModel(
field,
suggestionProvider,
fieldCheckers,
preferencesService,
undoManager);

keywordTagsField.setCellFactory(new ViewModelListCellFactory<Keyword>().withText(Keyword::get));
keywordTagsField.setTagViewFactory(this::createTag);

keywordTagsField.setSuggestionProvider(request -> viewModel.getSuggestions(request.getUserText()));
keywordTagsField.setConverter(viewModel.getStringConverter());
keywordTagsField.setMatcher((keyword, searchText) -> keyword.get().toLowerCase().startsWith(searchText.toLowerCase()));
keywordTagsField.setComparator(Comparator.comparing(Keyword::get));

keywordTagsField.setNewItemProducer(searchText -> viewModel.getStringConverter().fromString(searchText));

keywordTagsField.setShowSearchIcon(false);
keywordTagsField.getEditor().getStyleClass().clear();
keywordTagsField.getEditor().getStyleClass().add("tags-field-editor");

Bindings.bindContentBidirectional(keywordTagsField.getTags(), viewModel.keywordListProperty());
}

private Node createTag(Keyword keyword) {
Label tagLabel = new Label();
tagLabel.setText(keywordTagsField.getConverter().toString(keyword));
tagLabel.setGraphic(IconTheme.JabRefIcons.REMOVE_TAGS.getGraphicNode());
tagLabel.getGraphic().setOnMouseClicked(event -> keywordTagsField.removeTags(keyword));
tagLabel.setContentDisplay(ContentDisplay.RIGHT);
ContextMenu contextMenu = new ContextMenu();
ActionFactory factory = new ActionFactory(keyBindingRepository);
contextMenu.getItems().addAll(
factory.createMenuItem(StandardActions.COPY, new KeywordsEditor.TagContextAction(StandardActions.COPY, keyword)),
factory.createMenuItem(StandardActions.CUT, new KeywordsEditor.TagContextAction(StandardActions.CUT, keyword)),
factory.createMenuItem(StandardActions.DELETE, new KeywordsEditor.TagContextAction(StandardActions.DELETE, keyword))
);
tagLabel.setContextMenu(contextMenu);
return tagLabel;
}

public KeywordsEditorViewModel getViewModel() {
return viewModel;
}

@Override
public void bindToEntry(BibEntry entry) {
viewModel.bindToEntry(entry);
}

@Override
public Parent getNode() {
return this;
}

@Override
public void requestFocus() {
keywordTagsField.requestFocus();
}

@Override
public double getWeight() {
return 2;
}

private class TagContextAction extends SimpleCommand {
private final StandardActions command;
private final Keyword keyword;

public TagContextAction(StandardActions command, Keyword keyword) {
this.command = command;
this.keyword = keyword;
}

@Override
public void execute() {
switch (command) {
case COPY -> {
clipBoardManager.setContent(keyword.get());
dialogService.notify(Localization.lang("Copied '%0' to clipboard.",
JabRefDialogService.shortenDialogMessage(keyword.get())));
}
case CUT -> {
clipBoardManager.setContent(keyword.get());
dialogService.notify(Localization.lang("Copied '%0' to clipboard.",
JabRefDialogService.shortenDialogMessage(keyword.get())));
keywordTagsField.removeTags(keyword);
}
case DELETE ->
keywordTagsField.removeTags(keyword);
default ->
LOGGER.info("Action {} not defined", command.getText());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package org.jabref.gui.fieldeditors;

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

import javax.swing.undo.UndoManager;

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.util.StringConverter;

import org.jabref.gui.autocompleter.SuggestionProvider;
import org.jabref.gui.util.BindingsHelper;
import org.jabref.logic.integrity.FieldCheckers;
import org.jabref.model.entry.Keyword;
import org.jabref.model.entry.KeywordList;
import org.jabref.model.entry.field.Field;
import org.jabref.preferences.PreferencesService;

import org.tinylog.Logger;

public class KeywordsEditorViewModel extends AbstractEditorViewModel {

private final ListProperty<Keyword> keywordListProperty;
private final Character keywordSeparator;
private final SuggestionProvider<?> suggestionProvider;

public KeywordsEditorViewModel(Field field,
SuggestionProvider<?> suggestionProvider,
FieldCheckers fieldCheckers,
PreferencesService preferencesService,
UndoManager undoManager) {

super(field, suggestionProvider, fieldCheckers, undoManager);

keywordListProperty = new SimpleListProperty<>(FXCollections.observableArrayList());
this.keywordSeparator = preferencesService.getBibEntryPreferences().getKeywordSeparator();
this.suggestionProvider = suggestionProvider;

BindingsHelper.bindContentBidirectional(
keywordListProperty,
text,
this::serializeKeywords,
this::parseKeywords);
}

private String serializeKeywords(List<Keyword> keywords) {
return KeywordList.serialize(keywords, keywordSeparator);
}

private List<Keyword> parseKeywords(String newText) {
return KeywordList.parse(newText, keywordSeparator).stream().toList();
}

public ListProperty<Keyword> keywordListProperty() {
return keywordListProperty;
}

public StringConverter<Keyword> getStringConverter() {
return new StringConverter<>() {
@Override
public String toString(Keyword keyword) {
if (keyword == null) {
Logger.debug("Keyword is null");
return "";
}
return keyword.get();
}

@Override
public Keyword fromString(String keywordString) {
return new Keyword(keywordString);
}
};
}

public List<Keyword> getSuggestions(String request) {
List<Keyword> suggestions = suggestionProvider.getPossibleSuggestions().stream()
.map(String.class::cast)
.filter(keyword -> keyword.toLowerCase().contains(request.toLowerCase()))
.map(Keyword::new)
.distinct()
.collect(Collectors.toList());

Keyword requestedKeyword = new Keyword(request);
if (!suggestions.contains(requestedKeyword)) {
suggestions.addFirst(requestedKeyword);
}

return suggestions;
}
}
4 changes: 4 additions & 0 deletions src/main/java/org/jabref/model/entry/KeywordList.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ public static KeywordList parse(String keywordString, Character delimiter) {
return parse(keywordString, delimiter, Keyword.DEFAULT_HIERARCHICAL_DELIMITER);
}

public static String serialize(List<Keyword> keywords, Character delimiter) {
return keywords.stream().map(Keyword::get).collect(Collectors.joining(delimiter.toString()));
}

public static KeywordList merge(String keywordStringA, String keywordStringB, Character delimiter) {
KeywordList keywordListA = parse(keywordStringA, delimiter);
KeywordList keywordListB = parse(keywordStringB, delimiter);
Expand Down

0 comments on commit 8374693

Please sign in to comment.