From 8374693f49302d9c1489a0d9b633ca73825c7b79 Mon Sep 17 00:00:00 2001 From: Loay Ghreeb <52158423+LoayGhreeb@users.noreply.github.com> Date: Tue, 5 Mar 2024 02:15:26 +0200 Subject: [PATCH] Implemented TagsField for the Keywords field (#10910) * 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 * 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 --- CHANGELOG.md | 1 + .../jabref/gui/fieldeditors/FieldEditors.java | 2 +- .../gui/fieldeditors/KeywordsEditor.fxml | 8 + .../gui/fieldeditors/KeywordsEditor.java | 141 +++++++++++++++++- .../fieldeditors/KeywordsEditorViewModel.java | 93 ++++++++++++ .../org/jabref/model/entry/KeywordList.java | 4 + 6 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.fxml create mode 100644 src/main/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModel.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec35123236..125e8ca5b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java b/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java index e699a398451..ec921b2f523 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java +++ b/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java @@ -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)) { diff --git a/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.fxml b/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.fxml new file mode 100644 index 00000000000..0f96aca52a8 --- /dev/null +++ b/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.fxml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.java b/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.java index f11f7d1a362..2d79d22f0db 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.java @@ -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 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().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()); + } + } + } } diff --git a/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModel.java b/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModel.java new file mode 100644 index 00000000000..8accde67d6d --- /dev/null +++ b/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModel.java @@ -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 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 keywords) { + return KeywordList.serialize(keywords, keywordSeparator); + } + + private List parseKeywords(String newText) { + return KeywordList.parse(newText, keywordSeparator).stream().toList(); + } + + public ListProperty keywordListProperty() { + return keywordListProperty; + } + + public StringConverter 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 getSuggestions(String request) { + List 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; + } +} diff --git a/src/main/java/org/jabref/model/entry/KeywordList.java b/src/main/java/org/jabref/model/entry/KeywordList.java index d54de57f977..76adb9add8d 100644 --- a/src/main/java/org/jabref/model/entry/KeywordList.java +++ b/src/main/java/org/jabref/model/entry/KeywordList.java @@ -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 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);