diff --git a/CHANGELOG.md b/CHANGELOG.md index 07af6f1e7c4..24cde49aee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We fixed "The library has been modified by another program" showing up when line breaks change [#4877](https://github.com/JabRef/jabref/issues/4877) - The default directory of the "LaTeX Citations" tab is now the directory of the currently opened database (and not the directory chosen at the last open file dialog or the last database save) [koppor#538](https://github.com/koppor/jabref/issues/538) - When writing a bib file, the `NegativeArraySizeException` should not occur [#8231](https://github.com/JabRef/jabref/issues/8231) [#8265](https://github.com/JabRef/jabref/issues/8265) +- We fixed an issue where some menu entries were available without entries selected. [#4795](https://github.com/JabRef/jabref/issues/4795) - We fixed an issue where right-clicking on a tab and selecting close will close the focused tab even if it is not the tab we right-clicked [#8193](https://github.com/JabRef/jabref/pull/8193) - We fixed an issue where selecting a citation style in the preferences would sometimes produce an exception [#7860](https://github.com/JabRef/jabref/issues/7860) - We fixed an issue where an exception would occur when clicking on a DOI link in the preview pane [#7706](https://github.com/JabRef/jabref/issues/7706) diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 61ee16bc03b..281e59e313c 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -712,8 +712,8 @@ private MenuBar createMenu() { factory.createMenuItem(StandardActions.IMPORT_INTO_NEW_LIBRARY, new ImportCommand(this, true, prefs, stateManager))), factory.createSubMenu(StandardActions.EXPORT, - factory.createMenuItem(StandardActions.EXPORT_ALL, new ExportCommand(this, false, prefs)), - factory.createMenuItem(StandardActions.EXPORT_SELECTED, new ExportCommand(this, true, prefs)), + factory.createMenuItem(StandardActions.EXPORT_ALL, new ExportCommand(false, stateManager, dialogService, prefs)), + factory.createMenuItem(StandardActions.EXPORT_SELECTED, new ExportCommand(true, stateManager, dialogService, prefs)), factory.createMenuItem(StandardActions.SAVE_SELECTED_AS_PLAIN_BIBTEX, new SaveAction(SaveAction.SaveMethod.SAVE_SELECTED, this, prefs, stateManager))), new SeparatorMenuItem(), @@ -745,7 +745,7 @@ private MenuBar createMenu() { factory.createMenuItem(StandardActions.COPY_KEY_AND_TITLE, new CopyMoreAction(StandardActions.COPY_KEY_AND_TITLE, dialogService, stateManager, Globals.getClipboardManager(), prefs)), factory.createMenuItem(StandardActions.COPY_KEY_AND_LINK, new CopyMoreAction(StandardActions.COPY_KEY_AND_LINK, dialogService, stateManager, Globals.getClipboardManager(), prefs)), factory.createMenuItem(StandardActions.COPY_CITATION_PREVIEW, new CopyCitationAction(CitationStyleOutputFormat.HTML, dialogService, stateManager, Globals.getClipboardManager(), prefs.getPreviewPreferences())), - factory.createMenuItem(StandardActions.EXPORT_SELECTED_TO_CLIPBOARD, new ExportToClipboardAction(this, dialogService, Globals.exportFactory, Globals.getClipboardManager(), Globals.TASK_EXECUTOR, prefs))), + factory.createMenuItem(StandardActions.EXPORT_SELECTED_TO_CLIPBOARD, new ExportToClipboardAction(dialogService, Globals.exportFactory, stateManager, Globals.getClipboardManager(), Globals.TASK_EXECUTOR, prefs))), factory.createMenuItem(StandardActions.PASTE, new EditAction(StandardActions.PASTE, this, stateManager)), diff --git a/src/main/java/org/jabref/gui/exporter/ExportCommand.java b/src/main/java/org/jabref/gui/exporter/ExportCommand.java index 005267ac841..4511c7107ac 100644 --- a/src/main/java/org/jabref/gui/exporter/ExportCommand.java +++ b/src/main/java/org/jabref/gui/exporter/ExportCommand.java @@ -1,6 +1,7 @@ package org.jabref.gui.exporter; import java.nio.file.Path; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -9,7 +10,8 @@ import org.jabref.gui.DialogService; import org.jabref.gui.Globals; -import org.jabref.gui.JabRefFrame; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.ActionHelper; import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.FileDialogConfiguration; @@ -22,6 +24,7 @@ import org.jabref.logic.layout.LayoutFormatterPreferences; import org.jabref.logic.util.io.FileUtil; import org.jabref.logic.xmp.XmpPreferences; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.preferences.PreferencesService; @@ -34,19 +37,26 @@ public class ExportCommand extends SimpleCommand { private static final Logger LOGGER = LoggerFactory.getLogger(ExportCommand.class); - private final JabRefFrame frame; private final boolean selectedOnly; + private final StateManager stateManager; private final PreferencesService preferences; private final DialogService dialogService; /** * @param selectedOnly true if only the selected entries should be exported, otherwise all entries are exported */ - public ExportCommand(JabRefFrame frame, boolean selectedOnly, PreferencesService preferences) { - this.frame = frame; + public ExportCommand(boolean selectedOnly, + StateManager stateManager, + DialogService dialogService, + PreferencesService preferences) { this.selectedOnly = selectedOnly; + this.stateManager = stateManager; this.preferences = preferences; - this.dialogService = frame.getDialogService(); + this.dialogService = dialogService; + + this.executable.bind(selectedOnly + ? ActionHelper.needsEntriesSelected(stateManager) + : ActionHelper.needsDatabase(stateManager)); } @Override @@ -83,18 +93,20 @@ private void export(Path file, FileChooser.ExtensionFilter selectedExtensionFilt List entries; if (selectedOnly) { // Selected entries - entries = frame.getCurrentLibraryTab().getSelectedEntries(); + entries = stateManager.getSelectedEntries(); } else { // All entries - entries = frame.getCurrentLibraryTab().getDatabase().getEntries(); + entries = stateManager.getActiveDatabase() + .map(BibDatabaseContext::getEntries) + .orElse(Collections.emptyList()); } // Set the global variable for this database's file directory before exporting, // so formatters can resolve linked files correctly. // (This is an ugly hack!) - Globals.prefs.fileDirForDatabase = frame.getCurrentLibraryTab() - .getBibDatabaseContext() - .getFileDirectories(preferences.getFilePreferences()); + Globals.prefs.fileDirForDatabase = stateManager.getActiveDatabase() + .map(db -> db.getFileDirectories(preferences.getFilePreferences())) + .orElse(List.of(preferences.getFilePreferences().getWorkingDirectory())); // Make sure we remember which filter was used, to set // the default for next time: @@ -104,25 +116,24 @@ private void export(Path file, FileChooser.ExtensionFilter selectedExtensionFilt final List finEntries = entries; BackgroundTask .wrap(() -> { - format.export(frame.getCurrentLibraryTab().getBibDatabaseContext(), + format.export(stateManager.getActiveDatabase().get(), file, - frame.getCurrentLibraryTab() - .getBibDatabaseContext() + stateManager.getActiveDatabase().get() .getMetaData() .getEncoding() .orElse(preferences.getGeneralPreferences().getDefaultEncoding()), finEntries); return null; // can not use BackgroundTask.wrap(Runnable) because Runnable.run() can't throw Exceptions }) - .onSuccess(x -> frame.getDialogService().notify(Localization.lang("%0 export successful", format.getName()))) + .onSuccess(x -> dialogService.notify(Localization.lang("%0 export successful", format.getName()))) .onFailure(this::handleError) .executeWith(Globals.TASK_EXECUTOR); } private void handleError(Exception ex) { LOGGER.warn("Problem exporting", ex); - frame.getDialogService().notify(Localization.lang("Could not save file.")); + dialogService.notify(Localization.lang("Could not save file.")); // Need to warn the user that saving failed! - frame.getDialogService().showErrorDialogAndWait(Localization.lang("Save library"), Localization.lang("Could not save file."), ex); + dialogService.showErrorDialogAndWait(Localization.lang("Save library"), Localization.lang("Could not save file."), ex); } } diff --git a/src/main/java/org/jabref/gui/exporter/ExportToClipboardAction.java b/src/main/java/org/jabref/gui/exporter/ExportToClipboardAction.java index 456c32fb8bb..80247e2c335 100644 --- a/src/main/java/org/jabref/gui/exporter/ExportToClipboardAction.java +++ b/src/main/java/org/jabref/gui/exporter/ExportToClipboardAction.java @@ -2,11 +2,13 @@ import java.io.BufferedReader; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -15,8 +17,8 @@ import org.jabref.gui.ClipBoardManager; import org.jabref.gui.DialogService; -import org.jabref.gui.JabRefFrame; -import org.jabref.gui.LibraryTab; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.ActionHelper; import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; @@ -37,43 +39,42 @@ public class ExportToClipboardAction extends SimpleCommand { private static final Logger LOGGER = LoggerFactory.getLogger(ExportToClipboardAction.class); // Only text based exporters can be used - private static final Set SUPPORTED_FILETYPES = Set.of(StandardFileType.TXT, StandardFileType.RTF, StandardFileType.RDF, StandardFileType.XML, StandardFileType.HTML, StandardFileType.CSV, StandardFileType.RIS); + private static final Set SUPPORTED_FILETYPES = Set.of( + StandardFileType.TXT, + StandardFileType.RTF, + StandardFileType.RDF, + StandardFileType.XML, + StandardFileType.HTML, + StandardFileType.CSV, + StandardFileType.RIS); - private JabRefFrame frame; private final DialogService dialogService; - private LibraryTab panel; private final List entries = new ArrayList<>(); private final ExporterFactory exporterFactory; private final ClipBoardManager clipBoardManager; private final TaskExecutor taskExecutor; private final PreferencesService preferences; - - public ExportToClipboardAction(JabRefFrame frame, DialogService dialogService, ExporterFactory exporterFactory, ClipBoardManager clipBoardManager, TaskExecutor taskExecutor, PreferencesService prefs) { - this.frame = frame; - this.dialogService = dialogService; - this.exporterFactory = exporterFactory; - this.clipBoardManager = clipBoardManager; - this.taskExecutor = taskExecutor; - this.preferences = prefs; - } - - public ExportToClipboardAction(LibraryTab panel, DialogService dialogService, ExporterFactory exporterFactory, ClipBoardManager clipBoardManager, TaskExecutor taskExecutor, PreferencesService prefs) { - this.panel = panel; + private final StateManager stateManager; + + public ExportToClipboardAction(DialogService dialogService, + ExporterFactory exporterFactory, + StateManager stateManager, + ClipBoardManager clipBoardManager, + TaskExecutor taskExecutor, + PreferencesService prefs) { this.dialogService = dialogService; this.exporterFactory = exporterFactory; this.clipBoardManager = clipBoardManager; this.taskExecutor = taskExecutor; this.preferences = prefs; + this.stateManager = stateManager; + this.executable.bind(ActionHelper.needsEntriesSelected(stateManager)); } @Override public void execute() { - if (panel == null) { - panel = frame.getCurrentLibraryTab(); - } - - if (panel.getSelectedEntries().isEmpty()) { + if (stateManager.getSelectedEntries().isEmpty()) { dialogService.notify(Localization.lang("This operation requires one or more entries to be selected.")); return; } @@ -106,8 +107,9 @@ private ExportResult exportToClipboard(Exporter exporter) throws Exception { // Set the global variable for this database's file directory before exporting, // so formatters can resolve linked files correctly. // (This is an ugly hack!) - preferences.storeFileDirforDatabase(panel.getBibDatabaseContext() - .getFileDirectories(preferences.getFilePreferences())); + preferences.storeFileDirforDatabase(stateManager.getActiveDatabase() + .map(db -> db.getFileDirectories(preferences.getFilePreferences())) + .orElse(List.of(preferences.getFilePreferences().getWorkingDirectory()))); // Add chosen export type to last used preference, to become default preferences.getImportExportPreferences().setLastExportExtension(exporter.getName()); @@ -118,14 +120,14 @@ private ExportResult exportToClipboard(Exporter exporter) throws Exception { // file, and read the contents afterwards: tmp = Files.createTempFile("jabrefCb", ".tmp"); - entries.addAll(panel.getSelectedEntries()); + entries.addAll(stateManager.getSelectedEntries()); // Write to file: - exporter.export(panel.getBibDatabaseContext(), tmp, - panel.getBibDatabaseContext() - .getMetaData() - .getEncoding() - .orElse(preferences.getGeneralPreferences().getDefaultEncoding()), + exporter.export(stateManager.getActiveDatabase().get(), tmp, + stateManager.getActiveDatabase().get() + .getMetaData() + .getEncoding() + .orElse(preferences.getGeneralPreferences().getDefaultEncoding()), entries); // Read the file and put the contents on the clipboard: @@ -159,22 +161,17 @@ private void setContentToClipboard(ExportResult result) { } private String readFileToString(Path tmp) throws IOException { - try (BufferedReader reader = Files.newBufferedReader(tmp, panel.getBibDatabaseContext() - .getMetaData() - .getEncoding() - .orElse(preferences.getGeneralPreferences().getDefaultEncoding()))) { + Charset defaultEncoding = Objects.requireNonNull(preferences.getGeneralPreferences().getDefaultEncoding()); + try (BufferedReader reader = Files.newBufferedReader(tmp, stateManager.getActiveDatabase() + .map(db -> db.getMetaData() + .getEncoding() + .orElse(defaultEncoding)) + .orElse(defaultEncoding))) { return reader.lines().collect(Collectors.joining(OS.NEWLINE)); } } - private static class ExportResult { - - final String content; - final FileType fileType; + private record ExportResult(String content, FileType fileType) { - ExportResult(String content, FileType fileType) { - this.content = content; - this.fileType = fileType; - } } } diff --git a/src/main/java/org/jabref/gui/maintable/RightClickMenu.java b/src/main/java/org/jabref/gui/maintable/RightClickMenu.java index 31fd414d1b0..02bb314b407 100644 --- a/src/main/java/org/jabref/gui/maintable/RightClickMenu.java +++ b/src/main/java/org/jabref/gui/maintable/RightClickMenu.java @@ -110,7 +110,7 @@ private static Menu createCopySubMenu(LibraryTab libraryTab, copySpecialMenu.getItems().add(factory.createMenuItem(StandardActions.COPY_CITATION_PREVIEW, new CopyCitationAction(CitationStyleOutputFormat.HTML, dialogService, stateManager, clipBoardManager, previewPreferences))); } - copySpecialMenu.getItems().add(factory.createMenuItem(StandardActions.EXPORT_TO_CLIPBOARD, new ExportToClipboardAction(libraryTab, dialogService, Globals.exportFactory, clipBoardManager, Globals.TASK_EXECUTOR, preferencesService))); + copySpecialMenu.getItems().add(factory.createMenuItem(StandardActions.EXPORT_TO_CLIPBOARD, new ExportToClipboardAction(dialogService, Globals.exportFactory, stateManager, clipBoardManager, Globals.TASK_EXECUTOR, preferencesService))); return copySpecialMenu; } } diff --git a/src/test/java/org/jabref/gui/exporter/ExportToClipboardActionTest.java b/src/test/java/org/jabref/gui/exporter/ExportToClipboardActionTest.java index 2244bdb3c77..f0d1be6933d 100644 --- a/src/test/java/org/jabref/gui/exporter/ExportToClipboardActionTest.java +++ b/src/test/java/org/jabref/gui/exporter/ExportToClipboardActionTest.java @@ -4,15 +4,15 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + import org.jabref.gui.ClipBoardManager; import org.jabref.gui.DialogService; -import org.jabref.gui.JabRefFrame; -import org.jabref.gui.LibraryTab; +import org.jabref.gui.StateManager; import org.jabref.gui.util.CurrentThreadTaskExecutor; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.exporter.Exporter; @@ -30,6 +30,7 @@ import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.metadata.MetaData; +import org.jabref.preferences.FilePreferences; import org.jabref.preferences.GeneralPreferences; import org.jabref.preferences.ImportExportPreferences; import org.jabref.preferences.PreferencesService; @@ -50,29 +51,19 @@ public class ExportToClipboardActionTest { private ExportToClipboardAction exportToClipboardAction; - private final LibraryTab libraryTab = mock(LibraryTab.class); - private final JabRefFrame jabRefFrame = mock(JabRefFrame.class); private final DialogService dialogService = spy(DialogService.class); - private ExporterFactory exporterFactory; private final ClipBoardManager clipBoardManager = mock(ClipBoardManager.class); - private TaskExecutor taskExecutor; - private List selectedEntries; private final BibDatabaseContext databaseContext = mock(BibDatabaseContext.class); private final PreferencesService preferences = spy(PreferencesService.class); private final ImportExportPreferences importExportPrefs = mock(ImportExportPreferences.class); + private final StateManager stateManager = mock(StateManager.class); + + private ExporterFactory exporterFactory; + private TaskExecutor taskExecutor; + private ObservableList selectedEntries; @BeforeEach public void setUp() { - taskExecutor = new CurrentThreadTaskExecutor(); - - List customFormats = new ArrayList<>(); - LayoutFormatterPreferences layoutPreferences = mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS); - SavePreferences savePreferences = mock(SavePreferences.class); - XmpPreferences xmpPreferences = mock(XmpPreferences.class); - BibEntryTypesManager entryTypesManager = mock(BibEntryTypesManager.class); - exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences, xmpPreferences, BibDatabaseMode.BIBTEX, entryTypesManager); - exportToClipboardAction = new ExportToClipboardAction(libraryTab, dialogService, exporterFactory, clipBoardManager, taskExecutor, preferences); - BibEntry entry = new BibEntry(StandardEntryType.Misc) .withField(StandardField.AUTHOR, "Souti Chattopadhyay and Nicholas Nelson and Audrey Au and Natalia Morales and Christopher Sanchez and Rahul Pandita and Anita Sarma") .withField(StandardField.TITLE, "A tale from the trenches") @@ -80,13 +71,22 @@ public void setUp() { .withField(StandardField.DOI, "10.1145/3377811.3380330") .withField(StandardField.SUBTITLE, "cognitive biases and software development"); - selectedEntries = new ArrayList<>(); - selectedEntries.add(entry); + selectedEntries = FXCollections.observableArrayList(entry); + when(stateManager.getSelectedEntries()).thenReturn(selectedEntries); + + taskExecutor = new CurrentThreadTaskExecutor(); + List customFormats = new ArrayList<>(); + LayoutFormatterPreferences layoutPreferences = mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS); + SavePreferences savePreferences = mock(SavePreferences.class); + XmpPreferences xmpPreferences = mock(XmpPreferences.class); + BibEntryTypesManager entryTypesManager = mock(BibEntryTypesManager.class); + exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences, xmpPreferences, BibDatabaseMode.BIBTEX, entryTypesManager); + exportToClipboardAction = new ExportToClipboardAction(dialogService, exporterFactory, stateManager, clipBoardManager, taskExecutor, preferences); } @Test public void testExecuteIfNoSelectedEntries() { - when(libraryTab.getSelectedEntries()).thenReturn(Collections.EMPTY_LIST); + when(stateManager.getSelectedEntries()).thenReturn(FXCollections.emptyObservableList()); exportToClipboardAction.execute(); verify(dialogService, times(1)).notify(Localization.lang("This operation requires one or more entries to be selected.")); @@ -97,18 +97,21 @@ public void testExecuteOnSuccess() { Exporter selectedExporter = new Exporter("html", "HTML", StandardFileType.HTML) { @Override - public void export(BibDatabaseContext databaseContext, Path file, Charset encoding, List entries) throws Exception { + public void export(BibDatabaseContext databaseContext, Path file, Charset encoding, List entries) { } }; when(importExportPrefs.getLastExportExtension()).thenReturn("HTML"); when(preferences.getImportExportPreferences()).thenReturn(importExportPrefs); GeneralPreferences generalPreferences = mock(GeneralPreferences.class, Answers.RETURNS_DEEP_STUBS); + FilePreferences filePreferences = mock(FilePreferences.class, Answers.RETURNS_DEEP_STUBS); when(generalPreferences.getDefaultEncoding()).thenReturn(StandardCharsets.UTF_8); + when(preferences.getFilePreferences()).thenReturn(filePreferences); when(preferences.getGeneralPreferences()).thenReturn(generalPreferences); - when(libraryTab.getSelectedEntries()).thenReturn(selectedEntries); - when(libraryTab.getBibDatabaseContext()).thenReturn(databaseContext); - when(databaseContext.getFileDirectories(preferences.getFilePreferences())).thenReturn(new ArrayList<>(Arrays.asList(Path.of("path")))); + when(stateManager.getSelectedEntries()).thenReturn(selectedEntries); + when(stateManager.getActiveDatabase()).thenReturn(Optional.ofNullable(databaseContext)); + // noinspection ConstantConditions since databaseContext is mocked + when(databaseContext.getFileDirectories(preferences.getFilePreferences())).thenReturn(new ArrayList<>(List.of(Path.of("path")))); when(databaseContext.getMetaData()).thenReturn(new MetaData()); when(dialogService.showChoiceDialogAndWait( eq(Localization.lang("Export")), eq(Localization.lang("Select export format")), @@ -116,8 +119,8 @@ public void export(BibDatabaseContext databaseContext, Path file, Charset encodi exportToClipboardAction.execute(); verify(dialogService, times(1)).showChoiceDialogAndWait( - eq(Localization.lang("Export")), eq(Localization.lang("Select export format")), + eq(Localization.lang("Export")), eq(Localization.lang("Select export format")), eq(Localization.lang("Export")), any(Exporter.class), anyCollection()); verify(dialogService, times(1)).notify(Localization.lang("Entries exported to clipboard") + ": " + selectedEntries.size()); - } + } }