diff --git a/CHANGELOG.md b/CHANGELOG.md index c942e0a1ed2..2b96f17b6c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We added a feature that allows the user to open all linked files of multiple selected entries by "Open file" option. [#6966](https://github.com/JabRef/jabref/issues/6966) - We added a keybinding preset for new entries. [#7705](https://github.com/JabRef/jabref/issues/7705) - We added a select all button for the library import function. [#7786](https://github.com/JabRef/jabref/issues/7786) -- We added auto-key-generation progress to the background task list. [#7267](https://github.com/JabRef/jabref/issues/7267) +- We added a search feature for journal abbreviations. [#7804](https://github.com/JabRef/jabref/pull/7804) +- We added auto-key-generation progress to the background task list. [#7267](https://github.com/JabRef/jabref/issues/72) ### Changed diff --git a/src/main/java/org/jabref/gui/preferences/journals/AbbreviationViewModel.java b/src/main/java/org/jabref/gui/preferences/journals/AbbreviationViewModel.java index 26bf67cb1ec..b8f294fb06a 100644 --- a/src/main/java/org/jabref/gui/preferences/journals/AbbreviationViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/journals/AbbreviationViewModel.java @@ -1,5 +1,6 @@ package org.jabref.gui.preferences.journals; +import java.util.Locale; import java.util.Objects; import javafx.beans.property.BooleanProperty; @@ -94,4 +95,11 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(getName(), isPseudoAbbreviation()); } + + public boolean containsCaseIndependent(String searchTerm) { + searchTerm = searchTerm.toLowerCase(Locale.ROOT); + return this.abbreviation.get().toLowerCase(Locale.ROOT).contains(searchTerm) || + this.name.get().toLowerCase(Locale.ROOT).contains(searchTerm) || + this.shortestUniqueAbbreviation.get().toLowerCase(Locale.ROOT).contains(searchTerm); + } } diff --git a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.fxml b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.fxml index 3f4a342f534..0dc1c91d3f0 100644 --- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.fxml +++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.fxml @@ -10,6 +10,8 @@ + + @@ -64,4 +66,9 @@ + + + + + diff --git a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java index 0db240a26e9..c89d1c0e11c 100644 --- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java +++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java @@ -2,6 +2,16 @@ import javax.inject.Inject; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.transformation.FilteredList; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; @@ -10,16 +20,20 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.paint.Color; +import javafx.util.Duration; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.preferences.AbstractPreferenceTabView; import org.jabref.gui.preferences.PreferencesTab; +import org.jabref.gui.util.ColorUtil; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; import com.airhacks.afterburner.views.ViewLoader; import com.tobiasdiez.easybind.EasyBind; +import org.controlsfx.control.textfield.CustomTextField; /** * This class controls the user interface of the journal abbreviations dialog. The UI elements and their layout are @@ -33,6 +47,7 @@ public class JournalAbbreviationsTab extends AbstractPreferenceTabView journalTableNameColumn; @FXML private TableColumn journalTableAbbreviationColumn; @FXML private TableColumn journalTableShortestUniqueAbbreviationColumn; + private FilteredList filteredAbbreviations; @FXML private ComboBox journalFilesBox; @FXML private Button addAbbreviationButton; @FXML private Button removeAbbreviationButton; @@ -40,9 +55,15 @@ public class JournalAbbreviationsTab extends AbstractPreferenceTabView flashingColor; + private StringProperty flashingColorStringProperty; + public JournalAbbreviationsTab() { ViewLoader.view(this) .root(this) @@ -53,9 +74,15 @@ public JournalAbbreviationsTab() { private void initialize() { viewModel = new JournalAbbreviationsTabViewModel(preferencesService, dialogService, taskExecutor, abbreviationRepository); + filteredAbbreviations = new FilteredList<>(viewModel.abbreviationsProperty()); + setButtonStyles(); setUpTable(); setBindings(); + setAnimations(); + + searchBox.setPromptText(Localization.lang("Search") + "..."); + searchBox.setLeft(IconTheme.JabRefIcons.SEARCH.getGraphicNode()); } private void setButtonStyles() { @@ -78,7 +105,7 @@ private void setUpTable() { } private void setBindings() { - journalAbbreviationsTable.itemsProperty().bindBidirectional(viewModel.abbreviationsProperty()); + journalAbbreviationsTable.setItems(filteredAbbreviations); EasyBind.subscribe(journalAbbreviationsTable.getSelectionModel().selectedItemProperty(), newValue -> viewModel.currentAbbreviationProperty().set(newValue)); @@ -98,6 +125,27 @@ private void setBindings() { loadingLabel.visibleProperty().bind(viewModel.isLoadingProperty()); progressIndicator.visibleProperty().bind(viewModel.isLoadingProperty()); + + searchBox.textProperty().addListener((observable, previousText, searchTerm) -> { + filteredAbbreviations.setPredicate(abbreviation -> searchTerm.isEmpty() ? true : abbreviation.containsCaseIndependent(searchTerm)); + }); + } + + private void setAnimations() { + flashingColor = new SimpleObjectProperty<>(Color.TRANSPARENT); + flashingColorStringProperty = createFlashingColorStringProperty(flashingColor); + searchBox.styleProperty().bind( + new SimpleStringProperty("-fx-control-inner-background: ").concat(flashingColorStringProperty).concat(";") + ); + invalidateSearch = new Timeline( + new KeyFrame(Duration.seconds(0), new KeyValue(flashingColor, Color.TRANSPARENT, Interpolator.LINEAR)), + new KeyFrame(Duration.seconds(0.25), new KeyValue(flashingColor, Color.RED, Interpolator.LINEAR)), + new KeyFrame(Duration.seconds(0.25), new KeyValue(searchBox.textProperty(), "", Interpolator.DISCRETE)), + new KeyFrame(Duration.seconds(0.25), (ActionEvent event) -> { + addAbbreviationActions(); + }), + new KeyFrame(Duration.seconds(0.5), new KeyValue(flashingColor, Color.TRANSPARENT, Interpolator.LINEAR)) + ); } @FXML @@ -117,11 +165,30 @@ private void removeList() { @FXML private void addAbbreviation() { + if (!searchBox.getText().isEmpty()) { + invalidateSearch.play(); + } else { + addAbbreviationActions(); + } + } + + private void addAbbreviationActions() { viewModel.addAbbreviation(); selectNewAbbreviation(); editAbbreviation(); } + private static StringProperty createFlashingColorStringProperty(final ObjectProperty flashingColor) { + final StringProperty flashingColorStringProperty = new SimpleStringProperty(); + setColorStringFromColor(flashingColorStringProperty, flashingColor); + flashingColor.addListener((observable, oldValue, newValue) -> setColorStringFromColor(flashingColorStringProperty, flashingColor)); + return flashingColorStringProperty; + } + + private static void setColorStringFromColor(StringProperty colorStringProperty, ObjectProperty color) { + colorStringProperty.set(ColorUtil.toRGBACode(color.get())); + } + @FXML private void editAbbreviation() { journalAbbreviationsTable.edit( @@ -138,7 +205,7 @@ private void selectNewAbbreviation() { int lastRow = viewModel.abbreviationsCountProperty().get() - 1; journalAbbreviationsTable.scrollTo(lastRow); journalAbbreviationsTable.getSelectionModel().select(lastRow); - journalAbbreviationsTable.getFocusModel().focus(lastRow); + journalAbbreviationsTable.getFocusModel().focus(lastRow, journalTableNameColumn); } @Override diff --git a/src/main/java/org/jabref/gui/util/ColorUtil.java b/src/main/java/org/jabref/gui/util/ColorUtil.java index 19a8be2fd52..326c7f0f27d 100644 --- a/src/main/java/org/jabref/gui/util/ColorUtil.java +++ b/src/main/java/org/jabref/gui/util/ColorUtil.java @@ -11,6 +11,14 @@ public static String toRGBCode(Color color) { (int) (color.getBlue() * 255)); } + public static String toRGBACode(Color color) { + return String.format("rgba(%d,%d,%d,%f)", + (int) (color.getRed() * 255), + (int) (color.getGreen() * 255), + (int) (color.getBlue() * 255), + color.getOpacity()); + } + public static String toHex(Color validFieldBackgroundColor) { return String.format("#%02x%02x%02x", (int) validFieldBackgroundColor.getRed(), (int) validFieldBackgroundColor.getGreen(), (int) validFieldBackgroundColor.getBlue()); } diff --git a/src/test/java/org/jabref/gui/preferences/journals/AbbreviationViewModelTest.java b/src/test/java/org/jabref/gui/preferences/journals/AbbreviationViewModelTest.java new file mode 100644 index 00000000000..87f9ea5aeab --- /dev/null +++ b/src/test/java/org/jabref/gui/preferences/journals/AbbreviationViewModelTest.java @@ -0,0 +1,44 @@ +package org.jabref.gui.preferences.journals; + +import java.util.stream.Stream; + +import org.jabref.logic.journals.Abbreviation; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AbbreviationViewModelTest { + + @ParameterizedTest + @MethodSource("provideContainsCaseIndependentContains") + void containsCaseIndependentContains(String searchTerm, AbbreviationViewModel abbreviation) { + assertTrue(abbreviation.containsCaseIndependent(searchTerm)); + } + + private static Stream provideContainsCaseIndependentContains() { + return Stream.of( + Arguments.of("name", new AbbreviationViewModel(new Abbreviation("Long Name", "abbr", "unique"))), + Arguments.of("bBr", new AbbreviationViewModel(new Abbreviation("Long Name", "abbr", "unique"))), + Arguments.of("Uniq", new AbbreviationViewModel(new Abbreviation("Long Name", "abbr", "unique"))), + Arguments.of("", new AbbreviationViewModel(new Abbreviation("Long Name", "abbr", "unique"))), + Arguments.of("", new AbbreviationViewModel(new Abbreviation("", "", ""))) + ); + } + + @ParameterizedTest + @MethodSource("provideContainsCaseIndependentDoesNotContain") + void containsCaseIndependentDoesNotContain(String searchTerm, AbbreviationViewModel abbreviation) { + assertFalse(abbreviation.containsCaseIndependent(searchTerm)); + } + + private static Stream provideContainsCaseIndependentDoesNotContain() { + return Stream.of( + Arguments.of("Something else", new AbbreviationViewModel(new Abbreviation("Long Name", "abbr", "unique"))), + Arguments.of("Something", new AbbreviationViewModel(new Abbreviation("", "", ""))) + ); + } +} diff --git a/src/test/java/org/jabref/gui/util/ColorUtilTest.java b/src/test/java/org/jabref/gui/util/ColorUtilTest.java index a5f3a552841..a04489ac908 100644 --- a/src/test/java/org/jabref/gui/util/ColorUtilTest.java +++ b/src/test/java/org/jabref/gui/util/ColorUtilTest.java @@ -1,26 +1,51 @@ package org.jabref.gui.util; +import java.util.stream.Stream; + import javafx.scene.paint.Color; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; public class ColorUtilTest { + private static final Color C1 = Color.color(0.2, 0.4, 1); + private static final Color C2 = Color.rgb(255, 255, 255); + private static final Color C3 = Color.color(0, 0, 0, 0); + private static final Color C4 = Color.color(1, 1, 1, 1); + private static final Color C5 = Color.color(0.6, 0.8, 0.5, 0.3); + private ColorUtil colorUtil = new ColorUtil(); - private final Color c1 = Color.color(0.2, 0.4, 1); - private final Color c2 = Color.rgb(255, 255, 255); @Test public void toRGBCodeTest() { - assertEquals("#3366FF", ColorUtil.toRGBCode(c1)); - assertEquals("#FFFFFF", ColorUtil.toRGBCode(c2)); + assertEquals("#3366FF", ColorUtil.toRGBCode(C1)); + assertEquals("#FFFFFF", ColorUtil.toRGBCode(C2)); + } + + @ParameterizedTest + @MethodSource("provideToRGBACodeTest") + public void toRGBACodeTest(Color color, String expected) { + assertEquals(expected, ColorUtil.toRGBACode(color)); + } + + private static Stream provideToRGBACodeTest() { + return Stream.of( + Arguments.of(C1, String.format("rgba(51,102,255,%f)", 1.0)), + Arguments.of(C2, String.format("rgba(255,255,255,%f)", 1.0)), + Arguments.of(C3, String.format("rgba(0,0,0,%f)", 0.0)), + Arguments.of(C4, String.format("rgba(255,255,255,%f)", 1.0)), + Arguments.of(C5, String.format("rgba(153,204,127,%f)", 0.3)) + ); } @Test public void toHexTest() { - assertEquals("#000001", ColorUtil.toHex(c1)); - assertEquals("#010101", ColorUtil.toHex(c2)); + assertEquals("#000001", ColorUtil.toHex(C1)); + assertEquals("#010101", ColorUtil.toHex(C2)); } }