diff --git a/src/main/java/org/jabref/gui/FindUnlinkedFilesDialog.java b/src/main/java/org/jabref/gui/FindUnlinkedFilesDialog.java deleted file mode 100644 index e41f30dc616..00000000000 --- a/src/main/java/org/jabref/gui/FindUnlinkedFilesDialog.java +++ /dev/null @@ -1,1226 +0,0 @@ -package org.jabref.gui; - -import java.awt.Component; -import java.awt.Container; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.BorderFactory; -import javax.swing.DefaultListCellRenderer; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.JRootPane; -import javax.swing.JScrollPane; -import javax.swing.JTextField; -import javax.swing.JTree; -import javax.swing.KeyStroke; -import javax.swing.SwingConstants; -import javax.swing.WindowConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.filechooser.FileSystemView; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeCellRenderer; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreeModel; -import javax.swing.tree.TreeNode; -import javax.swing.tree.TreePath; - -import org.jabref.Globals; -import org.jabref.JabRefExecutorService; -import org.jabref.JabRefGUI; -import org.jabref.gui.desktop.JabRefDesktop; -import org.jabref.gui.externalfiletype.ExternalFileTypes; -import org.jabref.gui.importer.EntryFromFileCreator; -import org.jabref.gui.importer.EntryFromFileCreatorManager; -import org.jabref.gui.importer.UnlinkedFilesCrawler; -import org.jabref.gui.importer.UnlinkedPDFFileFilter; -import org.jabref.gui.util.DefaultTaskExecutor; -import org.jabref.gui.util.DirectoryDialogConfiguration; -import org.jabref.gui.util.FileDialogConfiguration; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.EntryTypes; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibtexEntryType; -import org.jabref.model.entry.EntryType; -import org.jabref.model.entry.FieldName; -import org.jabref.preferences.JabRefPreferences; - -import com.jgoodies.forms.builder.ButtonBarBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * GUI Dialog for the feature "Find unlinked files". - */ -public class FindUnlinkedFilesDialog extends JabRefDialog { - - private static final Logger LOGGER = LoggerFactory.getLogger(FindUnlinkedFilesDialog.class); - private static final String GLOBAL_PREFS_WORKING_DIRECTORY_KEY = "findUnlinkedFilesWD"; - - private static final String GLOBAL_PREFS_DIALOG_SIZE_KEY = "findUnlinkedFilesDialogSize"; - private final JabRefFrame frame; - private final BibDatabaseContext databaseContext; - private final EntryFromFileCreatorManager creatorManager; - - private final UnlinkedFilesCrawler crawler; - private Path lastSelectedDirectory; - - private TreeModel treeModel; - /* PANELS */ - private JPanel panelDirectory; - private JPanel panelSearchArea; - private JPanel panelFiles; - private JPanel panelOptions; - private JPanel panelButtons; - private JPanel panelEntryTypesSelection; - - private JPanel panelImportArea; - private JButton buttonBrowse; - private JButton buttonScan; - private JButton buttonExport; - private JButton buttonApply; - - private JButton buttonClose; - /* Options for the TreeView */ - private JButton buttonOptionSelectAll; - private JButton buttonOptionDeselectAll; - private JButton buttonOptionExpandAll; - private JButton buttonOptionCollapseAll; - - private JCheckBox checkboxCreateKeywords; - private JTextField textfieldDirectoryPath; - private JLabel labelDirectoryDescription; - private JLabel labelFileTypesDescription; - private JLabel labelFilesDescription; - private JLabel labelEntryTypeDescription; - private JLabel labelSearchingDirectoryInfo; - - private JLabel labelImportingInfo; - private JLabel labelExportingInfo; - private JTree tree; - private JScrollPane scrollpaneTree; - private JComboBox comboBoxFileTypeSelection; - - private JComboBox comboBoxEntryTypeSelection; - private JProgressBar progressBarSearching; - private JProgressBar progressBarImporting; - - private MouseListener treeMouseListener; - private Action actionSelectAll; - private Action actionUnselectAll; - private Action actionExpandTree; - - private Action actionCollapseTree; - - private ComponentListener dialogPositionListener; - private final AtomicBoolean threadState = new AtomicBoolean(); - - private boolean checkBoxWhyIsThereNoGetSelectedStupidSwing; - - public FindUnlinkedFilesDialog(JabRefFrame frame) { - super(Localization.lang("Find unlinked files"), true, FindUnlinkedFilesDialog.class); - this.frame = frame; - - restoreSizeOfDialog(); - - databaseContext = frame.getCurrentBasePanel().getBibDatabaseContext(); - creatorManager = new EntryFromFileCreatorManager(ExternalFileTypes.getInstance()); - - crawler = new UnlinkedFilesCrawler(databaseContext); - - lastSelectedDirectory = loadLastSelectedDirectory(); - - initialize(); - buttonApply.setEnabled(false); - buttonExport.setEnabled(false); - } - - /** - * Close dialog when pressing escape - */ - @Override - protected JRootPane createRootPane() { - ActionListener actionListener = actionEvent -> setVisible(false); - JRootPane rPane = new JRootPane(); - KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); - rPane.registerKeyboardAction(actionListener, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); - - return rPane; - } - - /** - * Stores the current size of this dialog persistently. - */ - private void storeSizeOfDialog() { - Dimension dim = getSize(); - String store = dim.width + ";" + dim.height; - Globals.prefs.put(FindUnlinkedFilesDialog.GLOBAL_PREFS_DIALOG_SIZE_KEY, store); - } - - /** - * Restores the location and size of this dialog from the persistent storage. - */ - private void restoreSizeOfDialog() { - - String store = Globals.prefs.get(FindUnlinkedFilesDialog.GLOBAL_PREFS_DIALOG_SIZE_KEY); - - Dimension dimension = null; - - if (store != null) { - try { - String[] dim = store.split(";"); - dimension = new Dimension(Integer.valueOf(dim[0]), Integer.valueOf(dim[1])); - } catch (NumberFormatException ignoredEx) { - LOGGER.debug("RestoreSizeDialog Exception ", ignoredEx); - } - } - if (dimension != null) { - setPreferredSize(dimension); - } - } - - /** - * Initializes the components, the layout, the data structure and the - * actions in this dialog. - */ - private void initialize() { - - initializeActions(); - initComponents(); - createTree(); - createFileTypesCombobox(); - createEntryTypesCombobox(); - initLayout(); - setupActions(); - pack(); - } - - /** - * Initializes action objects.
- * Does not assign actions to components yet! - */ - private void initializeActions() { - - actionSelectAll = new AbstractAction(Localization.lang("Select all")) { - - @Override - public void actionPerformed(ActionEvent e) { - CheckableTreeNode rootNode = (CheckableTreeNode) tree.getModel().getRoot(); - rootNode.setSelected(true); - tree.invalidate(); - tree.repaint(); - } - }; - - actionUnselectAll = new AbstractAction(Localization.lang("Unselect all")) { - - @Override - public void actionPerformed(ActionEvent e) { - CheckableTreeNode rootNode = (CheckableTreeNode) tree.getModel().getRoot(); - rootNode.setSelected(false); - tree.invalidate(); - tree.repaint(); - } - }; - - actionExpandTree = new AbstractAction(Localization.lang("Expand all")) { - - @Override - public void actionPerformed(ActionEvent e) { - CheckableTreeNode rootNode = (CheckableTreeNode) tree.getModel().getRoot(); - expandTree(tree, new TreePath(rootNode), true); - } - }; - - actionCollapseTree = new AbstractAction(Localization.lang("Collapse all")) { - - @Override - public void actionPerformed(ActionEvent e) { - CheckableTreeNode rootNode = (CheckableTreeNode) tree.getModel().getRoot(); - expandTree(tree, new TreePath(rootNode), false); - } - }; - - dialogPositionListener = new ComponentAdapter() { - - /* (non-Javadoc) - * @see java.awt.event.ComponentAdapter#componentResized(java.awt.event.ComponentEvent) - */ - @Override - public void componentResized(ComponentEvent e) { - storeSizeOfDialog(); - } - - /* (non-Javadoc) - * @see java.awt.event.ComponentAdapter#componentMoved(java.awt.event.ComponentEvent) - */ - @Override - public void componentMoved(ComponentEvent e) { - storeSizeOfDialog(); - } - }; - - } - - /** - * Stores the working directory path for this view in the global - * preferences. - * - * @param lastSelectedDir - * directory that is used as the working directory in this view. - */ - private void storeLastSelectedDirectory(Path lastSelectedDir) { - lastSelectedDirectory = lastSelectedDir; - if (lastSelectedDirectory != null) { - Globals.prefs.put(FindUnlinkedFilesDialog.GLOBAL_PREFS_WORKING_DIRECTORY_KEY, - lastSelectedDirectory.toAbsolutePath().toString()); - } - } - - /** - * Loads the working directory path which is persistantly stored for this - * view and returns it as a {@link File}-Object.
- *
- * If there is no working directory path stored, the general working - * directory will be consulted. - * - * @return The persistently stored working directory path for this view. - */ - private Path loadLastSelectedDirectory() { - String workingDirectory = Globals.prefs.get(FindUnlinkedFilesDialog.GLOBAL_PREFS_WORKING_DIRECTORY_KEY); - if (workingDirectory == null) { - workingDirectory = Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY); - } - lastSelectedDirectory = Paths.get(workingDirectory); - - return lastSelectedDirectory; - } - - /** - * Disables or enables all visible Elements in this Dialog.
- *
- * This also removes the {@link MouseListener} from the Tree-View to prevent - * it from receiving mouse events when in disabled-state. - * - * @param enable - * true when the elements shall get enabled, - * false when they shall get disabled. - */ - private void disOrEnableDialog(boolean enable) { - - if (enable) { - tree.addMouseListener(treeMouseListener); - } else { - tree.removeMouseListener(treeMouseListener); - } - disOrEnableAllElements(FindUnlinkedFilesDialog.this, enable); - } - - /** - * Recursively disables or enables all swing and awt components in this - * dialog, starting with but not including the container - * startContainer. - * - * @param startContainer - * The GUI Element to start with. - * @param enable - * true, if all elements will get enabled, - * false if all elements will get disabled. - */ - private void disOrEnableAllElements(Container startContainer, boolean enable) { - Component[] children = startContainer.getComponents(); - for (Component child : children) { - if (child instanceof Container) { - disOrEnableAllElements((Container) child, enable); - } - child.setEnabled(enable); - } - } - - /** - * Expands or collapses the specified tree according to the - * expand-parameter. - */ - private void expandTree(JTree currentTree, TreePath parent, boolean expand) { - TreeNode node = (TreeNode) parent.getLastPathComponent(); - if (node.getChildCount() >= 0) { - for (Enumeration e = node.children(); e.hasMoreElements();) { - TreePath path = parent.pathByAddingChild(e.nextElement()); - expandTree(currentTree, path, expand); - } - } - if (expand) { - currentTree.expandPath(parent); - } else { - currentTree.collapsePath(parent); - } - } - - /** - * Starts the search of unlinked files according to the current dialog - * state.
- *
- * This state is made of:
- *
  • The value of the "directory"-input-textfield and
  • The file type - * selection.
    - * The search will process in a seperate thread and the progress bar behind - * the "search" button will be displayed.
    - *
    - * When the search has completed, the - * {@link #searchFinishedHandler(CheckableTreeNode)} handler method is - * invoked. - */ - private void startSearch() { - - Path directory = Paths.get(textfieldDirectoryPath.getText()); - if (Files.notExists(directory)) { - directory = Paths.get(System.getProperty("user.dir")); - } - if (!Files.isDirectory(directory)) { - directory = directory.getParent(); - } - - //this addtional statement is needed because for the lamdba the variable must be effetively final - Path dir = directory; - - storeLastSelectedDirectory(directory); - - progressBarSearching.setMinimumSize( - new Dimension(buttonScan.getSize().width, progressBarSearching.getMinimumSize().height)); - progressBarSearching.setVisible(true); - progressBarSearching.setString(""); - - labelSearchingDirectoryInfo.setVisible(true); - buttonScan.setVisible(false); - - disOrEnableDialog(false); - labelSearchingDirectoryInfo.setEnabled(true); - - final FileFilter selectedFileFilter = (FileFilter) comboBoxFileTypeSelection.getSelectedItem(); - - threadState.set(true); - JabRefExecutorService.INSTANCE.execute(() -> { - UnlinkedPDFFileFilter unlinkedPDFFileFilter = new UnlinkedPDFFileFilter(selectedFileFilter, - databaseContext); - CheckableTreeNode rootNode = crawler.searchDirectory(dir.toFile(), unlinkedPDFFileFilter, threadState, - new ChangeListener() { - - int counter; - - @Override - public void stateChanged(ChangeEvent e) { - counter++; - String message; - if (counter == 1) { - message = Localization.lang("One file found"); - } else { - message = Localization.lang("%0 files found", Integer.toString(counter)); - } - progressBarSearching.setString(message); - } - }); - searchFinishedHandler(rootNode); - }); - - } - - /** - * This will start the import of all file of all selected nodes in this - * dialogs tree view.
    - *
    - * The import itself will run in a seperate thread, whilst this dialog will - * be showing a progress bar, until the thread has finished its work.
    - *
    - * When the import has finished, the {@link #importFinishedHandler(java.util.List)} is - * invoked. - */ - private void startImport() { - - if (treeModel == null) { - return; - } - setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); - - CheckableTreeNode root = (CheckableTreeNode) treeModel.getRoot(); - - final List fileList = getFileListFromNode(root); - - if ((fileList == null) || fileList.isEmpty()) { - return; - } - - progressBarImporting.setVisible(true); - labelImportingInfo.setVisible(true); - buttonExport.setVisible(false); - buttonApply.setVisible(false); - buttonClose.setVisible(false); - disOrEnableDialog(false); - - labelImportingInfo.setEnabled(true); - - progressBarImporting.setMinimum(0); - progressBarImporting.setMaximum(fileList.size()); - progressBarImporting.setValue(0); - progressBarImporting.setString(""); - - final EntryType entryType = ((BibtexEntryTypeWrapper) comboBoxEntryTypeSelection.getSelectedItem()) - .getEntryType(); - - threadState.set(true); - JabRefExecutorService.INSTANCE.execute(() -> { - List errors = new LinkedList<>(); - creatorManager.addEntriesFromFiles(fileList, databaseContext.getDatabase(), frame.getCurrentBasePanel(), - entryType, checkBoxWhyIsThereNoGetSelectedStupidSwing, new ChangeListener() { - - int counter; - - @Override - public void stateChanged(ChangeEvent e) { - counter++; - progressBarImporting.setValue(counter); - progressBarImporting.setString(Localization.lang("%0 of %1", Integer.toString(counter), - Integer.toString(progressBarImporting.getMaximum()))); - } - }, errors); - importFinishedHandler(errors); - }); - } - - /** - * This starts the export of all files of all selected nodes in this - * dialogs tree view.
    - *
    - * The export itself will run in a seperate thread, whilst this dialog will - * be showing a progress bar, until the thread has finished its work.
    - *
    - * When the export has finished, the {@link #exportFinishedHandler()} is - * invoked. - */ - private void startExport() { - if (treeModel == null) { - return; - } - setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); - - CheckableTreeNode root = (CheckableTreeNode) treeModel.getRoot(); - - final List fileList = getFileListFromNode(root); - if ((fileList == null) || fileList.isEmpty()) { - return; - } - - buttonExport.setVisible(false); - buttonApply.setVisible(false); - buttonClose.setVisible(false); - disOrEnableDialog(false); - - FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() - .withInitialDirectory(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY)).build(); - DialogService ds = new FXDialogService(); - - Optional exportPath = DefaultTaskExecutor - .runInJavaFXThread(() -> ds.showFileSaveDialog(fileDialogConfiguration)); - - if (!exportPath.isPresent()) { - exportFinishedHandler(); - return; - } - - threadState.set(true); - JabRefExecutorService.INSTANCE.execute(() -> { - try (BufferedWriter writer = - Files.newBufferedWriter(exportPath.get(), StandardCharsets.UTF_8, - StandardOpenOption.CREATE)) { - for (File file : fileList) { - writer.write(file.toString() + "\n"); - } - - } catch (IOException e) { - LOGGER.warn("IO Error.", e); - } - }); - - exportFinishedHandler(); - } - - private void exportFinishedHandler() { - buttonExport.setVisible(true); - buttonApply.setVisible(true); - buttonClose.setVisible(true); - disOrEnableDialog(true); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - frame.getCurrentBasePanel().markBaseChanged(); - } - - /** - * - * @param errors - */ - private void importFinishedHandler(List errors) { - - if ((errors != null) && !errors.isEmpty()) { - String message; - if (errors.size() == 1) { - message = Localization.lang("There was one file that could not be imported."); - } else { - message = Localization.lang("There were %0 files which could not be imported.", - Integer.toString(errors.size())); - } - JOptionPane.showMessageDialog(this, - Localization.lang("The import finished with warnings:") + "\n" + message, - Localization.lang("Warning"), JOptionPane.WARNING_MESSAGE); - } - - progressBarImporting.setVisible(false); - labelImportingInfo.setVisible(false); - buttonExport.setVisible(true); - buttonApply.setVisible(true); - buttonClose.setVisible(true); - disOrEnableDialog(true); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - frame.getCurrentBasePanel().markBaseChanged(); - } - - /** - * Will be called from the Thread in which the "unlinked files search" is - * processed. As the result of the search, the root node of the determined - * file structure is passed. - * - * @param rootNode - * The root of the file structure as the result of the search. - */ - private void searchFinishedHandler(CheckableTreeNode rootNode) { - treeModel = new DefaultTreeModel(rootNode); - tree.setModel(treeModel); - tree.setRootVisible(rootNode.getChildCount() > 0); - - tree.invalidate(); - tree.repaint(); - - progressBarSearching.setVisible(false); - labelSearchingDirectoryInfo.setVisible(false); - buttonScan.setVisible(true); - actionSelectAll.actionPerformed(null); - - disOrEnableDialog(true); - buttonApply.setEnabled(true); - buttonExport.setEnabled(true); - } - - /** - * Sets up the actions for the components. - */ - private void setupActions() { - - DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder() - .withInitialDirectory(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY)).build(); - DialogService ds = frame.getDialogService(); - /** - * Stores the selected directory. - */ - buttonBrowse.addActionListener(e -> { - Optional selectedDirectory = DefaultTaskExecutor - .runInJavaFXThread(() -> ds.showDirectorySelectionDialog(directoryDialogConfiguration)); - selectedDirectory.ifPresent(d -> { - textfieldDirectoryPath.setText(d.toAbsolutePath().toString()); - storeLastSelectedDirectory(d); - }); - }); - - buttonScan.addActionListener(e -> startSearch()); - - /** - * Action for the button "Import...".
    - *
    - * Actions on this button will start the import of all file of all - * selected nodes in this dialogs tree view.
    - */ - buttonExport.addActionListener(e -> startExport()); - buttonApply.addActionListener(e -> startImport()); - buttonClose.addActionListener(e -> dispose()); - } - - /** - * Creates a list of {@link File}s for all leaf nodes in the tree structure - * node, which have been marked as selected.
    - *
    - * Selected nodes correspond to those entries in the tree, - * whose checkbox is checked. - * - * SIDE EFFECT: The checked nodes are removed from the tree. - * - * @param node - * The root node representing a tree structure. - * @return A list of files of all checked leaf nodes. - */ - private List getFileListFromNode(CheckableTreeNode node) { - List filesList = new ArrayList<>(); - Enumeration children = node.depthFirstEnumeration(); - List nodesToRemove = new ArrayList<>(); - for (CheckableTreeNode child : Collections.list(children)) { - if (child.isLeaf() && child.isSelected()) { - File nodeFile = ((FileNodeWrapper) child.getUserObject()).file; - if ((nodeFile != null) && nodeFile.isFile()) { - filesList.add(nodeFile); - nodesToRemove.add(child); - } - } - } - - // remove imported files from tree - DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); - for (CheckableTreeNode nodeToRemove : nodesToRemove) { - DefaultMutableTreeNode parent = (DefaultMutableTreeNode) nodeToRemove.getParent(); - model.removeNodeFromParent(nodeToRemove); - - // remove empty parent node - while ((parent != null) && parent.isLeaf()) { - DefaultMutableTreeNode pp = (DefaultMutableTreeNode) parent.getParent(); - if (pp != null) { - model.removeNodeFromParent(parent); - } - parent = pp; - } - // TODO: update counter / see: getTreeCellRendererComponent for label generation - } - tree.invalidate(); - tree.repaint(); - - return filesList; - } - - /** - * Initializes the visible components in this dialog. - */ - private void initComponents() { - - this.addComponentListener(dialogPositionListener); - /* Interrupts the searchThread by setting the State-Array to 0 */ - this.addWindowListener(new WindowAdapter() { - - @Override - public void windowClosing(WindowEvent e) { - threadState.set(false); - } - }); - - panelDirectory = new JPanel(); - panelSearchArea = new JPanel(); - panelFiles = new JPanel(); - panelOptions = new JPanel(); - panelEntryTypesSelection = new JPanel(); - panelButtons = new JPanel(); - panelImportArea = new JPanel(); - - buttonBrowse = new JButton(Localization.lang("Browse")); - buttonBrowse.setMnemonic('B'); - buttonBrowse.setToolTipText(Localization.lang("Opens the file browser.")); - buttonScan = new JButton(Localization.lang("Scan directory")); - buttonScan.setMnemonic('S'); - buttonScan.setToolTipText(Localization.lang("Searches the selected directory for unlinked files.")); - buttonExport = new JButton(Localization.lang("Export")); - buttonExport.setMnemonic('E'); - buttonExport.setToolTipText(Localization.lang("Export to text file.")); - buttonApply = new JButton(Localization.lang("Apply")); - buttonApply.setMnemonic('I'); - buttonApply.setToolTipText(Localization.lang("Starts the import of BibTeX entries.")); - buttonClose = new JButton(Localization.lang("Close")); - buttonClose.setToolTipText(Localization.lang("Leave this dialog.")); - buttonClose.setMnemonic('C'); - - /* Options for the TreeView */ - buttonOptionSelectAll = new JButton(); - buttonOptionSelectAll.setMnemonic('A'); - buttonOptionSelectAll.setAction(actionSelectAll); - buttonOptionDeselectAll = new JButton(); - buttonOptionDeselectAll.setMnemonic('U'); - buttonOptionDeselectAll.setAction(actionUnselectAll); - buttonOptionExpandAll = new JButton(); - buttonOptionExpandAll.setMnemonic('E'); - buttonOptionExpandAll.setAction(actionExpandTree); - buttonOptionCollapseAll = new JButton(); - buttonOptionCollapseAll.setMnemonic('L'); - buttonOptionCollapseAll.setAction(actionCollapseTree); - - checkboxCreateKeywords = new JCheckBox(Localization.lang("Create directory based keywords")); - checkboxCreateKeywords - .setToolTipText(Localization.lang("Creates keywords in created entrys with directory pathnames")); - checkboxCreateKeywords.setSelected(checkBoxWhyIsThereNoGetSelectedStupidSwing); - checkboxCreateKeywords.addItemListener( - e -> checkBoxWhyIsThereNoGetSelectedStupidSwing = !checkBoxWhyIsThereNoGetSelectedStupidSwing); - - textfieldDirectoryPath = new JTextField(); - textfieldDirectoryPath - .setText(lastSelectedDirectory == null ? "" : lastSelectedDirectory.toAbsolutePath().toString()); - - labelDirectoryDescription = new JLabel(Localization.lang("Select a directory where the search shall start.")); - labelFileTypesDescription = new JLabel(Localization.lang("Select file type:")); - labelFilesDescription = new JLabel(Localization.lang("These files are not linked in the active library.")); - labelEntryTypeDescription = new JLabel(Localization.lang("Entry type to be created:")); - labelSearchingDirectoryInfo = new JLabel(Localization.lang("Searching file system...")); - labelSearchingDirectoryInfo.setHorizontalAlignment(SwingConstants.CENTER); - labelSearchingDirectoryInfo.setVisible(false); - labelImportingInfo = new JLabel(Localization.lang("Importing into Library...")); - labelImportingInfo.setHorizontalAlignment(SwingConstants.CENTER); - labelImportingInfo.setVisible(false); - labelExportingInfo = new JLabel(Localization.lang("Exporting into file...")); - labelExportingInfo.setHorizontalAlignment(SwingConstants.CENTER); - labelExportingInfo.setVisible(false); - - tree = new JTree(); - - scrollpaneTree = new JScrollPane(tree); - scrollpaneTree.setWheelScrollingEnabled(true); - - progressBarSearching = new JProgressBar(); - progressBarSearching.setIndeterminate(true); - progressBarSearching.setVisible(false); - progressBarSearching.setStringPainted(true); - - progressBarImporting = new JProgressBar(); - progressBarImporting.setIndeterminate(false); - progressBarImporting.setVisible(false); - progressBarImporting.setStringPainted(true); - - } - - /** - * Initializes the layout for the visible components in this menu. A - * {@link GridBagLayout} is used. - */ - private void initLayout() { - - GridBagLayout gbl = new GridBagLayout(); - - panelDirectory.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), - Localization.lang("Select directory"))); - panelFiles.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), - Localization.lang("Select files"))); - panelEntryTypesSelection.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), - Localization.lang("BibTeX entry creation"))); - - Insets basicInsets = new Insets(6, 6, 6, 6); - Insets smallInsets = new Insets(3, 2, 3, 1); - Insets noInsets = new Insets(0, 0, 0, 0); - - // x, y, w, h, wx,wy,ix,iy - FindUnlinkedFilesDialog.addComponent(gbl, panelSearchArea, buttonScan, GridBagConstraints.HORIZONTAL, - GridBagConstraints.EAST, noInsets, 0, 1, 1, 1, 1, 1, 40, 10); - FindUnlinkedFilesDialog.addComponent(gbl, panelSearchArea, labelSearchingDirectoryInfo, - GridBagConstraints.HORIZONTAL, GridBagConstraints.EAST, noInsets, 0, 2, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelSearchArea, progressBarSearching, GridBagConstraints.HORIZONTAL, - GridBagConstraints.EAST, noInsets, 0, 3, 1, 1, 0, 0, 0, 0); - - FindUnlinkedFilesDialog.addComponent(gbl, panelDirectory, labelDirectoryDescription, null, - GridBagConstraints.WEST, new Insets(6, 6, 0, 6), 0, 0, 3, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelDirectory, textfieldDirectoryPath, GridBagConstraints.HORIZONTAL, - null, basicInsets, 0, 1, 2, 1, 1, 1, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelDirectory, buttonBrowse, GridBagConstraints.HORIZONTAL, - GridBagConstraints.EAST, basicInsets, 2, 1, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelDirectory, labelFileTypesDescription, GridBagConstraints.NONE, - GridBagConstraints.WEST, new Insets(18, 6, 18, 3), 0, 3, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelDirectory, comboBoxFileTypeSelection, - GridBagConstraints.HORIZONTAL, GridBagConstraints.WEST, new Insets(18, 3, 18, 6), 1, 3, 1, 1, 1, 0, 0, - 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelDirectory, panelSearchArea, GridBagConstraints.HORIZONTAL, - GridBagConstraints.EAST, new Insets(18, 6, 18, 6), 2, 3, 1, 1, 0, 0, 0, 0); - - FindUnlinkedFilesDialog.addComponent(gbl, panelFiles, labelFilesDescription, GridBagConstraints.HORIZONTAL, - GridBagConstraints.WEST, new Insets(6, 6, 0, 6), 0, 0, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelFiles, scrollpaneTree, GridBagConstraints.BOTH, - GridBagConstraints.CENTER, basicInsets, 0, 1, 1, 1, 1, 1, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelFiles, panelOptions, GridBagConstraints.NONE, - GridBagConstraints.NORTHEAST, basicInsets, 1, 1, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelOptions, buttonOptionSelectAll, GridBagConstraints.HORIZONTAL, - GridBagConstraints.NORTH, noInsets, 0, 0, 1, 1, 1, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelOptions, buttonOptionDeselectAll, GridBagConstraints.HORIZONTAL, - GridBagConstraints.NORTH, noInsets, 0, 1, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelOptions, buttonOptionExpandAll, GridBagConstraints.HORIZONTAL, - GridBagConstraints.NORTH, new Insets(6, 0, 0, 0), 0, 2, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelOptions, buttonOptionCollapseAll, GridBagConstraints.HORIZONTAL, - GridBagConstraints.NORTH, noInsets, 0, 3, 1, 1, 0, 0, 0, 0); - - FindUnlinkedFilesDialog.addComponent(gbl, panelEntryTypesSelection, labelEntryTypeDescription, - GridBagConstraints.NONE, GridBagConstraints.WEST, basicInsets, 0, 0, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelEntryTypesSelection, comboBoxEntryTypeSelection, - GridBagConstraints.NONE, GridBagConstraints.WEST, basicInsets, 1, 0, 1, 1, 1, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelEntryTypesSelection, checkboxCreateKeywords, - GridBagConstraints.HORIZONTAL, GridBagConstraints.WEST, basicInsets, 0, 1, 2, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelImportArea, labelImportingInfo, GridBagConstraints.HORIZONTAL, - GridBagConstraints.CENTER, new Insets(6, 6, 0, 6), 0, 1, 1, 1, 1, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelImportArea, labelExportingInfo, GridBagConstraints.HORIZONTAL, - GridBagConstraints.CENTER, new Insets(6, 6, 0, 6), 0, 1, 1, 1, 1, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelImportArea, progressBarImporting, GridBagConstraints.HORIZONTAL, - GridBagConstraints.CENTER, new Insets(0, 6, 6, 6), 0, 2, 1, 1, 1, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, panelButtons, panelImportArea, GridBagConstraints.NONE, - GridBagConstraints.EAST, smallInsets, 1, 0, 1, 1, 0, 0, 0, 0); - - FindUnlinkedFilesDialog.addComponent(gbl, getContentPane(), panelDirectory, GridBagConstraints.HORIZONTAL, - GridBagConstraints.CENTER, basicInsets, 0, 0, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, getContentPane(), panelFiles, GridBagConstraints.BOTH, - GridBagConstraints.NORTHWEST, new Insets(12, 6, 2, 2), 0, 1, 1, 1, 1, 1, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, getContentPane(), panelEntryTypesSelection, - GridBagConstraints.HORIZONTAL, GridBagConstraints.SOUTHWEST, new Insets(12, 6, 2, 2), 0, 2, 1, 1, 0, 0, - 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, getContentPane(), panelButtons, GridBagConstraints.HORIZONTAL, - GridBagConstraints.CENTER, new Insets(10, 6, 10, 6), 0, 3, 1, 1, 0, 0, 0, 0); - - ButtonBarBuilder bb = new ButtonBarBuilder(); - bb.addGlue(); - bb.addButton(buttonExport); - bb.addButton(buttonApply); - bb.addButton(buttonClose); - bb.addGlue(); - - bb.getPanel().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - panelImportArea.add(bb.getPanel(), GridBagConstraints.NONE); - pack(); - - } - - /** - * Adds a component to a container, using the specified gridbag-layout and - * the supplied parameters.
    - *
    - * This method is simply used to ged rid of thousands of lines of code, - * which inevitably rise when layouts such as the gridbag-layout is being - * used. - * - * @param layout - * The layout to be used. - * @param container - * The {@link Container}, to which the component will be added. - * @param component - * An AWT {@link Component}, that will be added to the container. - * @param fill - * A constant describing the fill behaviour (see - * {@link GridBagConstraints}). Can be null, if no - * filling wants to be specified. - * @param anchor - * A constant describing the anchor of the element in its parent - * container (see {@link GridBagConstraints}). Can be - * null, if no specification is needed. - * @param gridX - * The relative grid-X coordinate. - * @param gridY - * The relative grid-Y coordinate. - * @param width - * The relative width of the component. - * @param height - * The relative height of the component. - * @param weightX - * A value for the horizontal weight. - * @param weightY - * A value for the vertical weight. - * @param insets - * Insets of the component. Can be null. - */ - private static void addComponent(GridBagLayout layout, Container container, Component component, Integer fill, - Integer anchor, Insets insets, int gridX, int gridY, int width, int height, double weightX, double weightY, - int ipadX, int ipadY) { - container.setLayout(layout); - GridBagConstraints constraints = new GridBagConstraints(); - constraints.gridx = gridX; - constraints.gridy = gridY; - constraints.gridwidth = width; - constraints.gridheight = height; - constraints.weightx = weightX; - constraints.weighty = weightY; - constraints.ipadx = ipadX; - constraints.ipady = ipadY; - if (fill != null) { - constraints.fill = fill; - } - if (insets != null) { - constraints.insets = insets; - } - if (anchor != null) { - constraints.anchor = anchor; - } - layout.setConstraints(component, constraints); - container.add(component); - } - - /** - * Creates the tree view, that holds the data structure.
    - *
    - * Initially, the root node is not visible, so that the tree appears empty at the beginning. - */ - private void createTree() { - - /** - * Mouse listener to listen for mouse events on the tree.
    - * This will mark the selected tree entry as "selected" or "unselected", - * which will cause this nodes checkbox to appear as either "checked" or - * "unchecked". - */ - treeMouseListener = new MouseAdapter() { - - @Override - public void mousePressed(MouseEvent e) { - int x = e.getX(); - int y = e.getY(); - - int row = tree.getRowForLocation(x, y); - - TreePath path = tree.getPathForRow(row); - if (path != null) { - CheckableTreeNode node = (CheckableTreeNode) path.getLastPathComponent(); - if (e.getClickCount() == 2) { - Object userObject = node.getUserObject(); - if ((userObject instanceof FileNodeWrapper) && node.isLeaf()) { - FileNodeWrapper fnw = (FileNodeWrapper) userObject; - try { - JabRefDesktop.openExternalViewer( - JabRefGUI.getMainFrame().getCurrentBasePanel().getBibDatabaseContext(), - fnw.file.getAbsolutePath(), FieldName.PDF); - } catch (IOException e1) { - LOGGER.info("Error opening file", e1); - } - } - } else { - node.check(); - tree.invalidate(); - tree.repaint(); - } - } - } - - }; - - CheckableTreeNode startNode = new CheckableTreeNode("ROOT"); - DefaultTreeModel model = new DefaultTreeModel(startNode); - - tree.setModel(model); - tree.setRootVisible(false); - - DefaultTreeCellRenderer renderer = new CheckboxTreeCellRenderer(); - tree.setCellRenderer(renderer); - - tree.addMouseListener(treeMouseListener); - - } - - /** - * Initialises the combobox that contains the available file types which - * bibtex entries can be created of. - */ - private void createFileTypesCombobox() { - - List fileFilterList = creatorManager.getFileFilterList(); - - comboBoxFileTypeSelection = new JComboBox<>(fileFilterList.toArray(new FileFilter[fileFilterList.size()])); - - comboBoxFileTypeSelection.setRenderer(new DefaultListCellRenderer() { - - /* (non-Javadoc) - * @see javax.swing.DefaultListCellRenderer#getListCellRendererComponent(javax.swing.JList, java.lang.Object, int, boolean, boolean) - */ - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { - JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, - cellHasFocus); - if (value instanceof EntryFromFileCreator) { - EntryFromFileCreator creator = (EntryFromFileCreator) value; - if (creator.getExternalFileType() != null) { - label.setIcon(creator.getExternalFileType().getIcon().getSmallIcon()); - } - } - return label; - } - }); - - } - - /** - * Creates the ComboBox-View for the Listbox that holds the Bibtex entry - * types. - */ - private void createEntryTypesCombobox() { - - Iterator iterator = EntryTypes - .getAllValues(frame.getCurrentBasePanel().getBibDatabaseContext().getMode()).iterator(); - List list = new ArrayList<>(); - list.add( - new BibtexEntryTypeWrapper(null)); - while (iterator.hasNext()) { - list.add(new BibtexEntryTypeWrapper(iterator.next())); - } - comboBoxEntryTypeSelection = new JComboBox<>(list.toArray(new BibtexEntryTypeWrapper[list.size()])); - } - - /** - * Wrapper for displaying the Type {@link BibtexEntryType} in a Combobox. - */ - private static class BibtexEntryTypeWrapper { - - private final EntryType entryType; - - BibtexEntryTypeWrapper(EntryType bibtexType) { - this.entryType = bibtexType; - } - - @Override - public String toString() { - if (entryType == null) { - return Localization.lang(""); - } - return entryType.getName(); - } - - public EntryType getEntryType() { - return entryType; - } - } - - public static class CheckableTreeNode extends DefaultMutableTreeNode { - - private boolean isSelected; - private final JCheckBox checkbox; - - public CheckableTreeNode(Object userObject) { - super(userObject); - checkbox = new JCheckBox(); - } - - /** - * @return the checkbox - */ - public JCheckBox getCheckbox() { - return checkbox; - } - - public void check() { - setSelected(!isSelected); - } - - public void setSelected(boolean bSelected) { - isSelected = bSelected; - Enumeration tmpChildren = this.children(); - for (CheckableTreeNode child : Collections.list(tmpChildren)) { - child.setSelected(bSelected); - } - - } - - public boolean isSelected() { - return isSelected; - } - - } - - private static class CheckboxTreeCellRenderer extends DefaultTreeCellRenderer { - - private final FileSystemView fsv = FileSystemView.getFileSystemView(); - - @Override - public Component getTreeCellRendererComponent(final JTree tree, Object value, boolean sel, boolean expanded, - boolean leaf, int row, boolean hasFocus) { - - Component nodeComponent = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, - hasFocus); - CheckableTreeNode node = (CheckableTreeNode) value; - - FileNodeWrapper userObject = (FileNodeWrapper) node.getUserObject(); - - JPanel newPanel = new JPanel(); - - JCheckBox checkbox = node.getCheckbox(); - checkbox.setSelected(node.isSelected()); - - try { - setIcon(fsv.getSystemIcon(userObject.file)); - } catch (Exception ignored) { - // Ignored - } - - newPanel.setBackground(nodeComponent.getBackground()); - checkbox.setBackground(nodeComponent.getBackground()); - - GridBagLayout gbl = new GridBagLayout(); - FindUnlinkedFilesDialog.addComponent(gbl, newPanel, checkbox, null, null, null, 0, 0, 1, 1, 0, 0, 0, 0); - FindUnlinkedFilesDialog.addComponent(gbl, newPanel, nodeComponent, GridBagConstraints.HORIZONTAL, null, - new Insets(1, 2, 0, 0), 1, 0, 1, 1, 1, 0, 0, 0); - - if (userObject.fileCount > 0) { - JLabel label = new JLabel( - "(" + userObject.fileCount + " file" + (userObject.fileCount > 1 ? "s" : "") + ")"); - FindUnlinkedFilesDialog.addComponent(gbl, newPanel, label, null, null, new Insets(1, 2, 0, 0), 2, 0, 1, - 1, 0, 0, 0, 0); - } - return newPanel; - } - - } - - public static class FileNodeWrapper { - - public final File file; - public final int fileCount; - - public FileNodeWrapper(File aFile) { - this(aFile, 0); - } - - /** - * @param aDirectory - * @param fileCount - */ - public FileNodeWrapper(File aDirectory, int fileCount) { - this.file = aDirectory; - this.fileCount = fileCount; - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return file.getName(); - } - } - -} diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 4803df84fa1..2531456c6f0 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -62,7 +62,6 @@ import org.jabref.gui.actions.DatabasePropertiesAction; import org.jabref.gui.actions.EditExternalFileTypesAction; import org.jabref.gui.actions.ErrorConsoleAction; -import org.jabref.gui.actions.FindUnlinkedFilesAction; import org.jabref.gui.actions.IntegrityCheckAction; import org.jabref.gui.actions.LookupIdentifierAction; import org.jabref.gui.actions.ManageCustomExportsAction; @@ -89,6 +88,7 @@ import org.jabref.gui.exporter.ExportToClipboardAction; import org.jabref.gui.exporter.SaveAllAction; import org.jabref.gui.exporter.SaveDatabaseAction; +import org.jabref.gui.externalfiles.FindUnlinkedFilesAction; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.help.AboutAction; import org.jabref.gui.help.HelpAction; diff --git a/src/main/java/org/jabref/gui/actions/FindUnlinkedFilesAction.java b/src/main/java/org/jabref/gui/externalfiles/FindUnlinkedFilesAction.java similarity index 77% rename from src/main/java/org/jabref/gui/actions/FindUnlinkedFilesAction.java rename to src/main/java/org/jabref/gui/externalfiles/FindUnlinkedFilesAction.java index d1a193857eb..5684869d9ff 100644 --- a/src/main/java/org/jabref/gui/actions/FindUnlinkedFilesAction.java +++ b/src/main/java/org/jabref/gui/externalfiles/FindUnlinkedFilesAction.java @@ -1,7 +1,7 @@ -package org.jabref.gui.actions; +package org.jabref.gui.externalfiles; -import org.jabref.gui.FindUnlinkedFilesDialog; import org.jabref.gui.JabRefFrame; +import org.jabref.gui.actions.SimpleCommand; public class FindUnlinkedFilesAction extends SimpleCommand { @@ -14,7 +14,7 @@ public FindUnlinkedFilesAction(JabRefFrame jabRefFrame) { @Override public void execute() { FindUnlinkedFilesDialog dlg = new FindUnlinkedFilesDialog(jabRefFrame); - dlg.setVisible(true); + dlg.showAndWait(); } } diff --git a/src/main/java/org/jabref/gui/externalfiles/FindUnlinkedFilesDialog.java b/src/main/java/org/jabref/gui/externalfiles/FindUnlinkedFilesDialog.java new file mode 100644 index 00000000000..5a8b0e8c5a8 --- /dev/null +++ b/src/main/java/org/jabref/gui/externalfiles/FindUnlinkedFilesDialog.java @@ -0,0 +1,440 @@ +package org.jabref.gui.externalfiles; + +import java.io.BufferedWriter; +import java.io.FileFilter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.CheckBoxTreeItem; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; + +import org.jabref.Globals; +import org.jabref.gui.DialogService; +import org.jabref.gui.JabRefFrame; +import org.jabref.gui.externalfiletype.ExternalFileTypes; +import org.jabref.gui.importer.EntryFromFileCreator; +import org.jabref.gui.importer.EntryFromFileCreatorManager; +import org.jabref.gui.importer.UnlinkedFilesCrawler; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.DirectoryDialogConfiguration; +import org.jabref.gui.util.FileDialogConfiguration; +import org.jabref.gui.util.ViewModelListCellFactory; +import org.jabref.gui.util.ViewModelTreeCellFactory; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.EntryTypes; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.EntryType; +import org.jabref.preferences.JabRefPreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GUI Dialog for the feature "Find unlinked files". + */ +public class FindUnlinkedFilesDialog extends BaseDialog { + + private static final Logger LOGGER = LoggerFactory.getLogger(FindUnlinkedFilesDialog.class); + private final JabRefFrame frame; + private final BibDatabaseContext databaseContext; + private final EntryFromFileCreatorManager creatorManager; + private final JabRefPreferences preferences = Globals.prefs; + private final DialogService dialogService; + private Button buttonScan; + private Button buttonExport; + private Button buttonApply; + private CheckBox checkboxCreateKeywords; + private TextField textfieldDirectoryPath; + private TreeView tree; + private ComboBox comboBoxFileTypeSelection; + private ComboBox comboBoxEntryTypeSelection; + private VBox panelSearchProgress; + private BackgroundTask findUnlinkedFilesTask; + + public FindUnlinkedFilesDialog(JabRefFrame frame) { + super(); + this.setTitle(Localization.lang("Find unlinked files")); + this.frame = frame; + dialogService = frame.getDialogService(); + + databaseContext = frame.getCurrentBasePanel().getBibDatabaseContext(); + creatorManager = new EntryFromFileCreatorManager(ExternalFileTypes.getInstance()); + + initialize(); + } + + /** + * Initializes the components, the layout, the data structure and the actions in this dialog. + */ + private void initialize() { + Button buttonBrowse = new Button(Localization.lang("Browse")); + buttonBrowse.setTooltip(new Tooltip(Localization.lang("Opens the file browser."))); + buttonBrowse.getStyleClass().add("text-button"); + buttonBrowse.setOnAction(e -> { + DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder() + .withInitialDirectory(preferences.get(JabRefPreferences.WORKING_DIRECTORY)).build(); + dialogService.showDirectorySelectionDialog(directoryDialogConfiguration) + .ifPresent(selectedDirectory -> { + textfieldDirectoryPath.setText(selectedDirectory.toAbsolutePath().toString()); + preferences.put(JabRefPreferences.WORKING_DIRECTORY, selectedDirectory.toAbsolutePath().toString()); + }); + }); + + buttonScan = new Button(Localization.lang("Scan directory")); + buttonScan.setTooltip(new Tooltip((Localization.lang("Searches the selected directory for unlinked files.")))); + buttonScan.setOnAction(e -> startSearch()); + buttonScan.setDefaultButton(true); + buttonScan.setPadding(new Insets(5, 0, 0, 0)); + + buttonExport = new Button(Localization.lang("Export selected entries")); + buttonExport.setTooltip(new Tooltip(Localization.lang("Export to text file."))); + buttonExport.getStyleClass().add("text-button"); + buttonExport.setDisable(true); + buttonExport.setOnAction(e -> startExport()); + + ButtonType buttonTypeImport = new ButtonType(Localization.lang("Import"), ButtonBar.ButtonData.OK_DONE); + getDialogPane().getButtonTypes().setAll( + buttonTypeImport, + ButtonType.CANCEL + ); + buttonApply = (Button) getDialogPane().lookupButton(buttonTypeImport); + buttonApply.setTooltip(new Tooltip((Localization.lang("Starts the import of BibTeX entries.")))); + buttonApply.setDisable(true); + + /* Actions for the TreeView */ + Button buttonOptionSelectAll = new Button(); + buttonOptionSelectAll.setText(Localization.lang("Select all")); + buttonOptionSelectAll.getStyleClass().add("text-button"); + buttonOptionSelectAll.setOnAction(event -> { + CheckBoxTreeItem root = (CheckBoxTreeItem) tree.getRoot(); + // Need to toggle a twice to make sure everything is selected + root.setSelected(true); + root.setSelected(false); + root.setSelected(true); + }); + Button buttonOptionDeselectAll = new Button(); + buttonOptionDeselectAll.setText(Localization.lang("Unselect all")); + buttonOptionDeselectAll.getStyleClass().add("text-button"); + buttonOptionDeselectAll.setOnAction(event -> { + CheckBoxTreeItem root = (CheckBoxTreeItem) tree.getRoot(); + // Need to toggle a twice to make sure nothing is selected + root.setSelected(false); + root.setSelected(true); + root.setSelected(false); + }); + Button buttonOptionExpandAll = new Button(); + buttonOptionExpandAll.setText(Localization.lang("Expand all")); + buttonOptionExpandAll.getStyleClass().add("text-button"); + buttonOptionExpandAll.setOnAction(event -> { + CheckBoxTreeItem root = (CheckBoxTreeItem) tree.getRoot(); + expandTree(root, true); + }); + Button buttonOptionCollapseAll = new Button(); + buttonOptionCollapseAll.setText(Localization.lang("Collapse all")); + buttonOptionCollapseAll.getStyleClass().add("text-button"); + buttonOptionCollapseAll.setOnAction(event -> { + CheckBoxTreeItem root = (CheckBoxTreeItem) tree.getRoot(); + expandTree(root, false); + root.setExpanded(true); + }); + + checkboxCreateKeywords = new CheckBox(Localization.lang("Create directory based keywords")); + checkboxCreateKeywords.setTooltip(new Tooltip((Localization.lang("Creates keywords in created entrys with directory pathnames")))); + + textfieldDirectoryPath = new TextField(); + Path initialPath = databaseContext.getFirstExistingFileDir(preferences.getFilePreferences()) + .orElse(preferences.getWorkingDir()); + textfieldDirectoryPath.setText(initialPath.toAbsolutePath().toString()); + + Label labelDirectoryDescription = new Label(Localization.lang("Select a directory where the search shall start.")); + Label labelFileTypesDescription = new Label(Localization.lang("Select file type:")); + Label labelFilesDescription = new Label(Localization.lang("These files are not linked in the active library.")); + Label labelEntryTypeDescription = new Label(Localization.lang("Entry type to be created:")); + Label labelSearchingDirectoryInfo = new Label(Localization.lang("Searching file system...")); + + tree = new TreeView<>(); + tree.setPrefWidth(Double.POSITIVE_INFINITY); + + ScrollPane scrollPaneTree = new ScrollPane(tree); + scrollPaneTree.setFitToWidth(true); + + ProgressIndicator progressBarSearching = new ProgressIndicator(); + progressBarSearching.setMaxSize(50, 50); + + setResultConverter(buttonPressed -> { + if (buttonPressed == buttonTypeImport) { + startImport(); + } else { + if (findUnlinkedFilesTask != null) { + findUnlinkedFilesTask.cancel(); + } + } + return null; + }); + + new ViewModelTreeCellFactory() + .withText(node -> { + if (Files.isRegularFile(node.path)) { + // File + return node.path.getFileName().toString(); + } else { + // Directory + return node.path.getFileName() + " (" + node.fileCount + " file" + (node.fileCount > 1 ? "s" : "") + ")"; + } + }) + .install(tree); + List fileFilterList = creatorManager.getFileFilterList(); + comboBoxFileTypeSelection = new ComboBox<>(FXCollections.observableArrayList(fileFilterList)); + comboBoxFileTypeSelection.getSelectionModel().selectFirst(); + new ViewModelListCellFactory() + .withText(Object::toString) + .withIcon(fileFilter -> { + if (fileFilter instanceof EntryFromFileCreator) { + EntryFromFileCreator creator = (EntryFromFileCreator) fileFilter; + if (creator.getExternalFileType() != null) { + return creator.getExternalFileType().getIcon(); + } + } + return null; + }) + .install(comboBoxFileTypeSelection); + + Collection entryTypes = EntryTypes.getAllValues(frame.getCurrentBasePanel().getBibDatabaseContext().getMode()); + comboBoxEntryTypeSelection = new ComboBox<>(FXCollections.observableArrayList(entryTypes)); + comboBoxEntryTypeSelection.getSelectionModel().selectFirst(); + new ViewModelListCellFactory() + .withText(EntryType::getName) + .install(comboBoxEntryTypeSelection); + + panelSearchProgress = new VBox(5, labelSearchingDirectoryInfo, progressBarSearching); + panelSearchProgress.toFront(); + panelSearchProgress.setVisible(false); + +// panelDirectory.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), +// Localization.lang("Select directory"))); +// panelFiles.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), +// Localization.lang("Select files"))); +// panelEntryTypesSelection.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), +// Localization.lang("BibTeX entry creation"))); + + VBox panelDirectory = new VBox(5); + panelDirectory.getChildren().setAll( + labelDirectoryDescription, + new HBox(10, textfieldDirectoryPath, buttonBrowse), + new HBox(15, labelFileTypesDescription, comboBoxFileTypeSelection), + buttonScan + ); + HBox.setHgrow(textfieldDirectoryPath, Priority.ALWAYS); + + StackPane stackPaneTree = new StackPane(scrollPaneTree, panelSearchProgress); + StackPane.setAlignment(panelSearchProgress, Pos.CENTER); + BorderPane panelFiles = new BorderPane(); + panelFiles.setTop(labelFilesDescription); + panelFiles.setCenter(stackPaneTree); + panelFiles.setBottom(new HBox(5, buttonOptionSelectAll, buttonOptionDeselectAll, buttonOptionExpandAll, buttonOptionCollapseAll, buttonExport)); + + VBox panelEntryTypesSelection = new VBox(5); + panelEntryTypesSelection.getChildren().setAll( + new HBox(15, labelEntryTypeDescription, comboBoxEntryTypeSelection), + checkboxCreateKeywords + ); + + VBox container = new VBox(20); + container.getChildren().addAll( + panelDirectory, + panelFiles, + panelEntryTypesSelection + ); + container.setPrefWidth(600); + getDialogPane().setContent(container); + } + + /** + * Expands or collapses the specified tree according to the expand-parameter. + */ + private void expandTree(TreeItem item, boolean expand) { + if (item != null && !item.isLeaf()) { + item.setExpanded(expand); + for (TreeItem child : item.getChildren()) { + expandTree(child, expand); + } + } + } + + /** + * Starts the search of unlinked files according chosen directory and the file type selection. The search will + * process in a separate thread and a progress indicator will be displayed. + */ + private void startSearch() { + Path directory = getSearchDirectory(); + FileFilter selectedFileFilter = comboBoxFileTypeSelection.getValue(); + + findUnlinkedFilesTask = new UnlinkedFilesCrawler(directory, selectedFileFilter, databaseContext) + .onRunning(() -> { + panelSearchProgress.setVisible(true); + buttonScan.setDisable(true); + tree.setRoot(null); + }) + .onFinished(() -> { + panelSearchProgress.setVisible(false); + buttonScan.setDisable(false); + }) + .onSuccess(root -> { + tree.setRoot(root); + root.setSelected(true); + root.setExpanded(true); + + buttonApply.setDisable(false); + buttonExport.setDisable(false); + buttonScan.setDefaultButton(false); + }); + findUnlinkedFilesTask.executeWith(Globals.TASK_EXECUTOR); + } + + private Path getSearchDirectory() { + Path directory = Paths.get(textfieldDirectoryPath.getText()); + if (Files.notExists(directory)) { + directory = Paths.get(System.getProperty("user.dir")); + textfieldDirectoryPath.setText(directory.toAbsolutePath().toString()); + } + if (!Files.isDirectory(directory)) { + directory = directory.getParent(); + textfieldDirectoryPath.setText(directory.toAbsolutePath().toString()); + } + return directory; + } + + /** + * This will start the import of all file of all selected nodes in the file tree view. + */ + private void startImport() { + CheckBoxTreeItem root = (CheckBoxTreeItem) tree.getRoot(); + final List fileList = getFileListFromNode(root); + + if (fileList.isEmpty()) { + return; + } + + final EntryType entryType = comboBoxEntryTypeSelection.getValue(); + + List errors = creatorManager.addEntriesFromFiles( + fileList, + databaseContext.getDatabase(), + frame.getCurrentBasePanel(), + entryType, + checkboxCreateKeywords.isSelected()); + + if (!errors.isEmpty()) { + String message; + if (errors.size() == 1) { + message = Localization.lang("There was one file that could not be imported."); + } else { + message = Localization.lang("There were %0 files which could not be imported.", + Integer.toString(errors.size())); + } + dialogService.showWarningDialogAndWait( + Localization.lang("Warning"), + Localization.lang("The import finished with warnings:") + "\n" + message); + } + } + + /** + * This starts the export of all files of all selected nodes in the file tree view. + */ + private void startExport() { + CheckBoxTreeItem root = (CheckBoxTreeItem) tree.getRoot(); + + final List fileList = getFileListFromNode(root); + if (fileList.isEmpty()) { + return; + } + + buttonExport.setVisible(false); + buttonApply.setVisible(false); + + FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() + .withInitialDirectory(preferences.get(JabRefPreferences.WORKING_DIRECTORY)).build(); + Optional exportPath = dialogService.showFileSaveDialog(fileDialogConfiguration); + + if (!exportPath.isPresent()) { + buttonExport.setVisible(true); + buttonApply.setVisible(true); + return; + } + + try (BufferedWriter writer = + Files.newBufferedWriter(exportPath.get(), StandardCharsets.UTF_8, + StandardOpenOption.CREATE)) { + for (Path file : fileList) { + writer.write(file.toString() + "\n"); + } + } catch (IOException e) { + LOGGER.warn("IO Error.", e); + } + + buttonExport.setVisible(true); + buttonApply.setVisible(true); + } + + /** + * Creates a list of all files (leaf nodes in the tree structure), which have been selected. + * + * @param node The root node representing a tree structure. + */ + private List getFileListFromNode(CheckBoxTreeItem node) { + List filesList = new ArrayList<>(); + for (TreeItem childNode : node.getChildren()) { + CheckBoxTreeItem child = (CheckBoxTreeItem) childNode; + if (child.isLeaf() && child.isSelected()) { + Path nodeFile = child.getValue().path; + if ((nodeFile != null) && Files.isRegularFile(nodeFile)) { + filesList.add(nodeFile); + } + } + } + return filesList; + } + + public static class FileNodeWrapper { + + public final Path path; + public final int fileCount; + + public FileNodeWrapper(Path path) { + this(path, 0); + } + + public FileNodeWrapper(Path path, int fileCount) { + this.path = path; + this.fileCount = fileCount; + } + } +} diff --git a/src/main/java/org/jabref/gui/importer/EntryFromFileCreatorManager.java b/src/main/java/org/jabref/gui/importer/EntryFromFileCreatorManager.java index c1a0d2d0a3d..79b1d93f582 100644 --- a/src/main/java/org/jabref/gui/importer/EntryFromFileCreatorManager.java +++ b/src/main/java/org/jabref/gui/importer/EntryFromFileCreatorManager.java @@ -2,14 +2,13 @@ import java.io.File; import java.io.FileFilter; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import java.util.Optional; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; import javax.swing.undo.CompoundEdit; import org.jabref.gui.BasePanel; @@ -67,22 +66,21 @@ private boolean hasSpecialisedCreatorForExternalFileType( } /** - * Returns a EntryFromFileCreator object that is capable of creating a - * BibEntry for the given File. + * Returns a {@link EntryFromFileCreator} object that is capable of creating a entry for the given file. * - * @param file the pdf file - * @return null if there is no EntryFromFileCreator for this File. + * @param file the external file + * @return an empty optional if there is no EntryFromFileCreator for this file. */ - public EntryFromFileCreator getEntryCreator(File file) { - if ((file == null) || !file.exists()) { - return null; + public Optional getEntryCreator(Path file) { + if (!Files.exists(file)) { + return Optional.empty(); } for (EntryFromFileCreator creator : entryCreators) { - if (creator.accept(file)) { - return creator; + if (creator.accept(file.toFile())) { + return Optional.of(creator); } } - return null; + return Optional.empty(); } /** @@ -93,43 +91,32 @@ public EntryFromFileCreator getEntryCreator(File file) { * @param entryType * @return List of unexpected import event messages including failures. */ - public List addEntrysFromFiles(List files, - BibDatabase database, EntryType entryType, - boolean generateKeywordsFromPathToFile) { - List importGUIMessages = new LinkedList<>(); - addEntriesFromFiles(files, database, null, entryType, - generateKeywordsFromPathToFile, null, importGUIMessages); - return importGUIMessages; + public List addEntrysFromFiles(List files, + BibDatabase database, EntryType entryType, + boolean generateKeywordsFromPathToFile) { + return addEntriesFromFiles(files, database, null, entryType, generateKeywordsFromPathToFile); } /** * Tries to add a entry for each file in the List. * - * @param files - * @param database - * @param panel - * @param entryType - * @param generateKeywordsFromPathToFile - * @param changeListener - * @param importGUIMessages list of unexpected import event - Messages including - * failures - * @return Returns The number of entries added + * @return Returns a list of unexpected failures while importing */ - public int addEntriesFromFiles(List files, - BibDatabase database, BasePanel panel, EntryType entryType, - boolean generateKeywordsFromPathToFile, - ChangeListener changeListener, List importGUIMessages) { + public List addEntriesFromFiles(List files, + BibDatabase database, BasePanel panel, EntryType entryType, + boolean generateKeywordsFromPathToFile) { + List importGUIMessages = new ArrayList<>(); int count = 0; CompoundEdit ce = new CompoundEdit(); - for (File f : files) { - EntryFromFileCreator creator = getEntryCreator(f); - if (creator == null) { - importGUIMessages.add("Problem importing " + f.getPath() + ": Unknown filetype."); + for (Path f : files) { + Optional creator = getEntryCreator(f); + if (!creator.isPresent()) { + importGUIMessages.add("Problem importing " + f.toAbsolutePath() + ": Unknown filetype."); } else { - Optional entry = creator.createEntry(f, generateKeywordsFromPathToFile); + Optional entry = creator.get().createEntry(f.toFile(), generateKeywordsFromPathToFile); if (!entry.isPresent()) { - importGUIMessages.add("Problem importing " + f.getPath() + ": Entry could not be created."); + importGUIMessages.add("Problem importing " + f.toAbsolutePath() + ": Entry could not be created."); continue; } if (entryType != null) { @@ -147,7 +134,7 @@ public int addEntriesFromFiles(List files, // Work around SIDE EFFECT of creator.createEntry. The EntryFromPDFCreator also creates the entry in the table // Therefore, we only insert the entry if it is not already present if (database.insertEntry(entry.get())) { - importGUIMessages.add("Problem importing " + f.getPath() + ": Insert into BibDatabase failed."); + importGUIMessages.add("Problem importing " + f.toAbsolutePath() + ": Insert into BibDatabase failed."); } else { count++; if (panel != null) { @@ -156,17 +143,13 @@ public int addEntriesFromFiles(List files, } } } - - if (changeListener != null) { - changeListener.stateChanged(new ChangeEvent(this)); - } } if ((count > 0) && (panel != null)) { ce.end(); panel.getUndoManager().addEdit(ce); } - return count; + return importGUIMessages; } @@ -211,12 +194,9 @@ public String toString() { * @return A List of all known possible file filters. */ public List getFileFilterList() { - List filters = new ArrayList<>(); filters.add(getFileFilter()); - for (FileFilter creator : entryCreators) { - filters.add(creator); - } + filters.addAll(entryCreators); return filters; } } diff --git a/src/main/java/org/jabref/gui/importer/UnlinkedFilesCrawler.java b/src/main/java/org/jabref/gui/importer/UnlinkedFilesCrawler.java index f3b456fa937..77e57a12ab9 100644 --- a/src/main/java/org/jabref/gui/importer/UnlinkedFilesCrawler.java +++ b/src/main/java/org/jabref/gui/importer/UnlinkedFilesCrawler.java @@ -2,38 +2,39 @@ import java.io.File; import java.io.FileFilter; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; +import javafx.scene.control.CheckBoxTreeItem; -import org.jabref.gui.FindUnlinkedFilesDialog.CheckableTreeNode; -import org.jabref.gui.FindUnlinkedFilesDialog.FileNodeWrapper; +import org.jabref.gui.externalfiles.FindUnlinkedFilesDialog.FileNodeWrapper; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; /** * Util class for searching files on the file system which are not linked to a provided {@link BibDatabase}. */ -public class UnlinkedFilesCrawler { - /** - * File filter, that accepts directories only. - */ - private static final FileFilter DIRECTORY_FILTER = pathname -> (pathname != null) && pathname.isDirectory(); +public class UnlinkedFilesCrawler extends BackgroundTask> { + private final Path directory; + private final FileFilter fileFilter; + private int counter; private final BibDatabaseContext databaseContext; - - public UnlinkedFilesCrawler(BibDatabaseContext databaseContext) { + public UnlinkedFilesCrawler(Path directory, FileFilter fileFilter, BibDatabaseContext databaseContext) { + this.directory = directory; + this.fileFilter = fileFilter; this.databaseContext = databaseContext; } - public CheckableTreeNode searchDirectory(File directory, FileFilter filter) { - UnlinkedPDFFileFilter ff = new UnlinkedPDFFileFilter(filter, databaseContext); - return searchDirectory(directory, ff, new AtomicBoolean(true), null); + @Override + protected CheckBoxTreeItem call() throws Exception { + UnlinkedPDFFileFilter unlinkedPDFFileFilter = new UnlinkedPDFFileFilter(fileFilter, databaseContext); + return searchDirectory(directory.toFile(), unlinkedPDFFileFilter); } /** @@ -43,7 +44,7 @@ public CheckableTreeNode searchDirectory(File directory, FileFilter filter) { * {@link EntryFromFileCreatorManager}, are taken into the resulting tree.
    *
    * The result will be a tree structure of nodes of the type - * {@link CheckableTreeNode}.
    + * {@link CheckBoxTreeItem}.
    *
    * The user objects that are attached to the nodes is the * {@link FileNodeWrapper}, which wraps the {@link File}-Object.
    @@ -53,11 +54,7 @@ public CheckableTreeNode searchDirectory(File directory, FileFilter filter) { * the recursion running. When the states value changes, the method will * resolve its recursion and return what it has saved so far. */ - public CheckableTreeNode searchDirectory(File directory, UnlinkedPDFFileFilter ff, AtomicBoolean state, ChangeListener changeListener) { - /* Cancellation of the search from outside! */ - if ((state == null) || !state.get()) { - return null; - } + private CheckBoxTreeItem searchDirectory(File directory, UnlinkedPDFFileFilter ff) { // Return null if the directory is not valid. if ((directory == null) || !directory.exists() || !directory.isDirectory()) { return null; @@ -70,11 +67,11 @@ public CheckableTreeNode searchDirectory(File directory, UnlinkedPDFFileFilter f } else { files = Arrays.asList(filesArray); } - CheckableTreeNode root = new CheckableTreeNode(null); + CheckBoxTreeItem root = new CheckBoxTreeItem<>(new FileNodeWrapper(directory.toPath(), 0)); int filesCount = 0; - filesArray = directory.listFiles(DIRECTORY_FILTER); + filesArray = directory.listFiles(pathname -> (pathname != null) && pathname.isDirectory()); List subDirectories; if (filesArray == null) { subDirectories = Collections.emptyList(); @@ -82,23 +79,30 @@ public CheckableTreeNode searchDirectory(File directory, UnlinkedPDFFileFilter f subDirectories = Arrays.asList(filesArray); } for (File subDirectory : subDirectories) { - CheckableTreeNode subRoot = searchDirectory(subDirectory, ff, state, changeListener); - if ((subRoot != null) && (subRoot.getChildCount() > 0)) { - filesCount += ((FileNodeWrapper) subRoot.getUserObject()).fileCount; - root.add(subRoot); + if (isCanceled()) { + return root; + } + + CheckBoxTreeItem subRoot = searchDirectory(subDirectory, ff); + if ((subRoot != null) && (!subRoot.getChildren().isEmpty())) { + filesCount += subRoot.getValue().fileCount; + root.getChildren().add(subRoot); } } - root.setUserObject(new FileNodeWrapper(directory, files.size() + filesCount)); + root.setValue(new FileNodeWrapper(directory.toPath(), files.size() + filesCount)); for (File file : files) { - root.add(new CheckableTreeNode(new FileNodeWrapper(file))); - if (changeListener != null) { - changeListener.stateChanged(new ChangeEvent(this)); + root.getChildren().add(new CheckBoxTreeItem<>(new FileNodeWrapper(file.toPath()))); + + counter++; + if (counter == 1) { + updateMessage(Localization.lang("One file found")); + } else { + updateMessage(Localization.lang("%0 files found", Integer.toString(counter))); } } return root; } - } diff --git a/src/main/java/org/jabref/gui/util/BackgroundTask.java b/src/main/java/org/jabref/gui/util/BackgroundTask.java index fbc2abb22e9..bab42164053 100644 --- a/src/main/java/org/jabref/gui/util/BackgroundTask.java +++ b/src/main/java/org/jabref/gui/util/BackgroundTask.java @@ -5,10 +5,14 @@ import java.util.function.Consumer; import java.util.function.Function; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.concurrent.Task; import org.fxmisc.easybind.EasyBind; @@ -26,7 +30,9 @@ public abstract class BackgroundTask { private Consumer onSuccess; private Consumer onException; private Runnable onFinished; + private BooleanProperty isCanceled = new SimpleBooleanProperty(false); private ObjectProperty progress = new SimpleObjectProperty<>(new BackgroundProgress(0, 0)); + private StringProperty message = new SimpleStringProperty(""); private DoubleProperty workDonePercentage = new SimpleDoubleProperty(0); public BackgroundTask() { @@ -52,6 +58,37 @@ protected Void call() throws Exception { }; } + private static Consumer chain(Runnable first, Consumer second) { + if (first != null) { + if (second != null) { + return result -> { + first.run(); + second.accept(result); + }; + } else { + return result -> first.run(); + } + } else { + return second; + } + } + + public boolean isCanceled() { + return isCanceled.get(); + } + + public void cancel() { + this.isCanceled.set(true); + } + + public BooleanProperty isCanceledProperty() { + return isCanceled; + } + + public StringProperty messageProperty() { + return message; + } + public double getWorkDonePercentage() { return workDonePercentage.get(); } @@ -68,21 +105,6 @@ public ObjectProperty progressProperty() { return progress; } - private static Consumer chain(Runnable first, Consumer second) { - if (first != null) { - if (second != null) { - return result -> { - first.run(); - second.accept(result); - }; - } else { - return result -> first.run(); - } - } else { - return second; - } - } - /** * Sets the {@link Runnable} that is invoked after the task is started. */ @@ -197,6 +219,10 @@ protected void updateProgress(double workDone, double max) { updateProgress(new BackgroundProgress(workDone, max)); } + protected void updateMessage(String newMessage) { + message.setValue(newMessage); + } + class BackgroundProgress { private final double workDone; diff --git a/src/main/java/org/jabref/gui/util/DefaultTaskExecutor.java b/src/main/java/org/jabref/gui/util/DefaultTaskExecutor.java index bfed247a73f..fe7f2f64611 100644 --- a/src/main/java/org/jabref/gui/util/DefaultTaskExecutor.java +++ b/src/main/java/org/jabref/gui/util/DefaultTaskExecutor.java @@ -103,6 +103,12 @@ private Task getJavaFXTask(BackgroundTask task) { { EasyBind.subscribe(task.progressProperty(), progress -> updateProgress(progress.getWorkDone(), progress.getMax())); + EasyBind.subscribe(task.messageProperty(), this::updateMessage); + EasyBind.subscribe(task.isCanceledProperty(), cancelled -> { + if (cancelled) { + cancel(); + } + }); } @Override diff --git a/src/main/java/org/jabref/gui/util/ViewModelListCellFactory.java b/src/main/java/org/jabref/gui/util/ViewModelListCellFactory.java index 87295e0b7dd..983e4c4b29e 100644 --- a/src/main/java/org/jabref/gui/util/ViewModelListCellFactory.java +++ b/src/main/java/org/jabref/gui/util/ViewModelListCellFactory.java @@ -49,7 +49,14 @@ public ViewModelListCellFactory withGraphic(Callback toGraphic) { } public ViewModelListCellFactory withIcon(Callback toIcon) { - this.toGraphic = viewModel -> MaterialDesignIconFactory.get().createIcon(toIcon.call(viewModel)); + this.toGraphic = viewModel -> { + GlyphIcons icon = toIcon.call(viewModel); + if (icon != null) { + return MaterialDesignIconFactory.get().createIcon(icon); + } else { + return null; + } + }; return this; } diff --git a/src/main/java/org/jabref/gui/util/ViewModelTreeCellFactory.java b/src/main/java/org/jabref/gui/util/ViewModelTreeCellFactory.java new file mode 100644 index 00000000000..c4d01133406 --- /dev/null +++ b/src/main/java/org/jabref/gui/util/ViewModelTreeCellFactory.java @@ -0,0 +1,84 @@ +package org.jabref.gui.util; + +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.control.CheckBoxTreeItem; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeTableCell; +import javafx.scene.control.TreeView; +import javafx.scene.control.cell.CheckBoxTreeCell; +import javafx.scene.input.MouseEvent; +import javafx.util.Callback; +import javafx.util.StringConverter; + +import org.jabref.gui.icon.JabRefIcon; + +/** + * Constructs a {@link TreeTableCell} based on the view model of the row and a bunch of specified converter methods. + * + * @param view model + * @param cell value + */ +public class ViewModelTreeCellFactory implements Callback, TreeCell> { + + private Callback toText; + private Callback toGraphic; + private Callback> toOnMouseClickedEvent; + private Callback toTooltip; + + public ViewModelTreeCellFactory withText(Callback toText) { + this.toText = toText; + return this; + } + + public ViewModelTreeCellFactory withGraphic(Callback toGraphic) { + this.toGraphic = toGraphic; + return this; + } + + public ViewModelTreeCellFactory withIcon(Callback toIcon) { + this.toGraphic = viewModel -> toIcon.call(viewModel).getGraphicNode(); + return this; + } + + public ViewModelTreeCellFactory withTooltip(Callback toTooltip) { + this.toTooltip = toTooltip; + return this; + } + + public ViewModelTreeCellFactory withOnMouseClickedEvent(Callback> toOnMouseClickedEvent) { + this.toOnMouseClickedEvent = toOnMouseClickedEvent; + return this; + } + + public void install(TreeView treeView) { + treeView.setCellFactory(this); + } + + @Override + public TreeCell call(TreeView tree) { + Callback, ObservableValue> getSelectedProperty = + item -> { + if (item instanceof CheckBoxTreeItem) { + return ((CheckBoxTreeItem) item).selectedProperty(); + } + return null; + }; + + StringConverter> converter = new StringConverter>() { + @Override + public String toString(TreeItem treeItem) { + return (treeItem == null || treeItem.getValue() == null || toText == null) ? + "" : toText.call(treeItem.getValue()); + } + + @Override + public TreeItem fromString(String string) { + throw new UnsupportedOperationException("Not supported."); + } + }; + return new CheckBoxTreeCell<>(getSelectedProperty, converter); + } +} diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index ea96020a5fb..45f724fc252 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -2012,5 +2012,4 @@ public Map getMainTableColumnSortTypes() { } return map; } - } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 65d77c60098..529922c4b94 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -1393,7 +1393,6 @@ Opens\ the\ file\ browser.=Opens the file browser. Scan\ directory=Scan directory Searches\ the\ selected\ directory\ for\ unlinked\ files.=Searches the selected directory for unlinked files. Starts\ the\ import\ of\ BibTeX\ entries.=Starts the import of BibTeX entries. -Leave\ this\ dialog.=Leave this dialog. Create\ directory\ based\ keywords=Create directory based keywords Creates\ keywords\ in\ created\ entrys\ with\ directory\ pathnames=Creates keywords in created entrys with directory pathnames Select\ a\ directory\ where\ the\ search\ shall\ start.=Select a directory where the search shall start. @@ -1401,12 +1400,9 @@ Select\ file\ type\:=Select file type: These\ files\ are\ not\ linked\ in\ the\ active\ library.=These files are not linked in the active library. Entry\ type\ to\ be\ created\:=Entry type to be created: Searching\ file\ system...=Searching file system... -Importing\ into\ Library...=Importing into Library... -Exporting\ into\ file...=Exporting into file... Select\ directory=Select directory Select\ files=Select files BibTeX\ entry\ creation=BibTeX entry creation -= Unable\ to\ connect\ to\ FreeCite\ online\ service.=Unable to connect to FreeCite online service. Parse\ with\ FreeCite=Parse with FreeCite How\ would\ you\ like\ to\ link\ to\ '%0'?=How would you like to link to '%0'? @@ -1981,7 +1977,6 @@ You\ must\ enter\ at\ least\ one\ field\ name=You must enter at least one field Non-ASCII\ encoded\ character\ found=Non-ASCII encoded character found Toggle\ web\ search\ interface=Toggle web search interface %0\ files\ found=%0 files found -%0\ of\ %1=%0 of %1 One\ file\ found=One file found The\ import\ finished\ with\ warnings\:=The import finished with warnings: There\ was\ one\ file\ that\ could\ not\ be\ imported.=There was one file that could not be imported. diff --git a/src/test/java/org/jabref/gui/importer/EntryFromFileCreatorManagerTest.java b/src/test/java/org/jabref/gui/importer/EntryFromFileCreatorManagerTest.java index ec04c0d99e7..49a5a5c8b91 100644 --- a/src/test/java/org/jabref/gui/importer/EntryFromFileCreatorManagerTest.java +++ b/src/test/java/org/jabref/gui/importer/EntryFromFileCreatorManagerTest.java @@ -1,21 +1,21 @@ package org.jabref.gui.importer; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.logic.importer.ImportDataTest; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.ParserResult; -import org.jabref.logic.importer.fileformat.BibtexParser; +import org.jabref.logic.importer.fileformat.BibtexImporter; import org.jabref.model.database.BibDatabase; -import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.FileFieldParser; +import org.jabref.model.entry.LinkedFile; import org.jabref.model.util.DummyFileUpdateMonitor; import org.jabref.testutils.category.GUITest; @@ -25,69 +25,61 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @GUITest -public class EntryFromFileCreatorManagerTest { +class EntryFromFileCreatorManagerTest { private final ImportFormatPreferences prefs = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); - private ExternalFileTypes externalFileTypes; + private EntryFromFileCreatorManager manager; @BeforeEach - public void setUp() { - externalFileTypes = mock(ExternalFileTypes.class, Answers.RETURNS_DEEP_STUBS); + void setUp() { + ExternalFileTypes externalFileTypes = mock(ExternalFileTypes.class, Answers.RETURNS_DEEP_STUBS); when(externalFileTypes.getExternalFileTypeByExt("pdf")).thenReturn(Optional.empty()); + manager = new EntryFromFileCreatorManager(externalFileTypes); } @Test - public void testGetCreator() { - EntryFromFileCreatorManager manager = new EntryFromFileCreatorManager(externalFileTypes); - EntryFromFileCreator creator = manager.getEntryCreator(ImportDataTest.NOT_EXISTING_PDF); - assertNull(creator); - - creator = manager.getEntryCreator(ImportDataTest.FILE_IN_DATABASE); - assertNotNull(creator); - assertTrue(creator.accept(ImportDataTest.FILE_IN_DATABASE)); + void getEntryCreatorForNotExistingPDFReturnsEmptyOptional() { + assertEquals(Optional.empty(), manager.getEntryCreator(ImportDataTest.NOT_EXISTING_PDF)); } @Test - public void testAddEntrysFromFiles() throws IOException { - try (FileInputStream stream = new FileInputStream(ImportDataTest.UNLINKED_FILES_TEST_BIB); - InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { - ParserResult result = new BibtexParser(prefs, new DummyFileUpdateMonitor()).parse(reader); - BibDatabase database = result.getDatabase(); - - List files = new ArrayList<>(); - - files.add(ImportDataTest.FILE_NOT_IN_DATABASE); - files.add(ImportDataTest.NOT_EXISTING_PDF); - - EntryFromFileCreatorManager manager = new EntryFromFileCreatorManager(externalFileTypes); - List errors = manager.addEntrysFromFiles(files, database, null, true); - - /** - * One file doesn't exist, so adding it as an entry should lead to an error message. - */ - assertEquals(1, errors.size()); - - boolean file1Found = false; - boolean file2Found = false; - for (BibEntry entry : database.getEntries()) { - String filesInfo = entry.getField("file").get(); - if (filesInfo.contains(files.get(0).getName())) { - file1Found = true; - } - if (filesInfo.contains(files.get(1).getName())) { - file2Found = true; - } - } - - assertTrue(file1Found); - assertFalse(file2Found); - } + void getEntryCreatorForExistingPDFReturnsCreatorAcceptingThatFile() { + Optional creator = manager.getEntryCreator(ImportDataTest.FILE_IN_DATABASE); + assertNotEquals(Optional.empty(), creator); + assertTrue(creator.get().accept(ImportDataTest.FILE_IN_DATABASE.toFile())); + } + + @Test + void testAddEntriesFromFiles() throws IOException { + ParserResult result = new BibtexImporter(prefs, new DummyFileUpdateMonitor()) + .importDatabase(ImportDataTest.UNLINKED_FILES_TEST_BIB, StandardCharsets.UTF_8); + BibDatabase database = result.getDatabase(); + + List files = new ArrayList<>(); + files.add(ImportDataTest.FILE_NOT_IN_DATABASE); + files.add(ImportDataTest.NOT_EXISTING_PDF); + + List errors = manager.addEntrysFromFiles(files, database, null, true); + + // One file doesn't exist, so adding it as an entry should lead to an error message. + assertEquals(1, errors.size()); + + Stream linkedFileStream = database.getEntries().stream() + .flatMap(entry -> FileFieldParser.parse(entry.getField("file").get()).stream()); + + boolean file1Found = linkedFileStream + .anyMatch(file -> file.getLink().equalsIgnoreCase(ImportDataTest.FILE_NOT_IN_DATABASE.toString())); + + boolean file2Found = linkedFileStream + .anyMatch(file -> file.getLink().equalsIgnoreCase(ImportDataTest.NOT_EXISTING_PDF.toString())); + + assertTrue(file1Found); + assertFalse(file2Found); } } diff --git a/src/test/java/org/jabref/gui/importer/EntryFromPDFCreatorTest.java b/src/test/java/org/jabref/gui/importer/EntryFromPDFCreatorTest.java index 575d82852b0..0a49334f501 100644 --- a/src/test/java/org/jabref/gui/importer/EntryFromPDFCreatorTest.java +++ b/src/test/java/org/jabref/gui/importer/EntryFromPDFCreatorTest.java @@ -43,17 +43,17 @@ public void testPDFFileFilter() { @Test public void testCreationOfEntryNoPDF() { - Optional entry = entryCreator.createEntry(ImportDataTest.NOT_EXISTING_PDF, false); + Optional entry = entryCreator.createEntry(ImportDataTest.NOT_EXISTING_PDF.toFile(), false); assertFalse(entry.isPresent()); } @Test @Disabled //Can't mock basepanel and maintable public void testCreationOfEntryNotInDatabase() { - Optional entry = entryCreator.createEntry(ImportDataTest.FILE_NOT_IN_DATABASE, false); + Optional entry = entryCreator.createEntry(ImportDataTest.FILE_NOT_IN_DATABASE.toFile(), false); assertTrue(entry.isPresent()); assertTrue(entry.get().getField("file").get().endsWith(":PDF")); - assertEquals(Optional.of(ImportDataTest.FILE_NOT_IN_DATABASE.getName()), + assertEquals(Optional.of(ImportDataTest.FILE_NOT_IN_DATABASE.getFileName().toString()), entry.get().getField("title")); } } diff --git a/src/test/java/org/jabref/logic/importer/DatabaseFileLookupTest.java b/src/test/java/org/jabref/logic/importer/DatabaseFileLookupTest.java index fdb97e6ed88..d5c93399861 100644 --- a/src/test/java/org/jabref/logic/importer/DatabaseFileLookupTest.java +++ b/src/test/java/org/jabref/logic/importer/DatabaseFileLookupTest.java @@ -1,11 +1,9 @@ package org.jabref.logic.importer; -import java.io.FileInputStream; -import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Collection; -import org.jabref.logic.importer.fileformat.BibtexParser; +import org.jabref.logic.importer.fileformat.BibtexImporter; import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import org.jabref.model.util.DummyFileUpdateMonitor; @@ -18,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.mock; -public class DatabaseFileLookupTest { +class DatabaseFileLookupTest { private BibDatabase database; private Collection entries; @@ -27,23 +25,21 @@ public class DatabaseFileLookupTest { private BibEntry entry2; @BeforeEach - public void setUp() throws Exception { - try (FileInputStream stream = new FileInputStream(ImportDataTest.UNLINKED_FILES_TEST_BIB); - InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { - ParserResult result = new BibtexParser(mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS), new DummyFileUpdateMonitor()).parse(reader); - database = result.getDatabase(); - entries = database.getEntries(); - - entry1 = database.getEntryByKey("entry1").get(); - entry2 = database.getEntryByKey("entry2").get(); - } + void setUp() throws Exception { + ParserResult result = new BibtexImporter(mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS), new DummyFileUpdateMonitor()) + .importDatabase(ImportDataTest.UNLINKED_FILES_TEST_BIB, StandardCharsets.UTF_8); + database = result.getDatabase(); + entries = database.getEntries(); + + entry1 = database.getEntryByKey("entry1").get(); + entry2 = database.getEntryByKey("entry2").get(); } /** * Tests the prerequisites of this test-class itself. */ @Test - public void testTestDatabase() { + void testTestDatabase() { assertEquals(2, database.getEntryCount()); assertEquals(2, entries.size()); assertNotNull(entry1); diff --git a/src/test/java/org/jabref/logic/importer/ImportDataTest.java b/src/test/java/org/jabref/logic/importer/ImportDataTest.java index dc457791f75..3a3fbb98c36 100644 --- a/src/test/java/org/jabref/logic/importer/ImportDataTest.java +++ b/src/test/java/org/jabref/logic/importer/ImportDataTest.java @@ -1,6 +1,7 @@ package org.jabref.logic.importer; -import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import org.junit.jupiter.api.Test; @@ -10,38 +11,29 @@ public class ImportDataTest { - public static final File FILE_IN_DATABASE = Paths - .get("src/test/resources/org/jabref/logic/importer/unlinkedFilesTestFolder/pdfInDatabase.pdf").toFile(); - public static final File FILE_NOT_IN_DATABASE = Paths - .get("src/test/resources/org/jabref/logic/importer/unlinkedFilesTestFolder/pdfNotInDatabase.pdf") - .toFile(); - public static final File EXISTING_FOLDER = Paths - .get("src/test/resources/org/jabref/logic/importer/unlinkedFilesTestFolder").toFile(); - public static final File NOT_EXISTING_FOLDER = Paths.get("notexistingfolder").toFile(); - public static final File NOT_EXISTING_PDF = Paths - .get("src/test/resources/org/jabref/logic/importer/unlinkedFilesTestFolder/null.pdf").toFile(); - public static final File UNLINKED_FILES_TEST_BIB = Paths - .get("src/test/resources/org/jabref/util/unlinkedFilesTestBib.bib").toFile(); + public static final Path FILE_IN_DATABASE = Paths.get("src/test/resources/org/jabref/logic/importer/unlinkedFilesTestFolder/pdfInDatabase.pdf"); + public static final Path FILE_NOT_IN_DATABASE = Paths.get("src/test/resources/org/jabref/logic/importer/unlinkedFilesTestFolder/pdfNotInDatabase.pdf"); + public static final Path EXISTING_FOLDER = Paths.get("src/test/resources/org/jabref/logic/importer/unlinkedFilesTestFolder"); + public static final Path NOT_EXISTING_FOLDER = Paths.get("notexistingfolder"); + public static final Path NOT_EXISTING_PDF = Paths.get("src/test/resources/org/jabref/logic/importer/unlinkedFilesTestFolder/null.pdf"); + public static final Path UNLINKED_FILES_TEST_BIB = Paths.get("src/test/resources/org/jabref/util/unlinkedFilesTestBib.bib"); /** * Tests the testing environment. */ @Test - public void testTestingEnvironment() { + void testTestingEnvironment() { - assertTrue(ImportDataTest.EXISTING_FOLDER.exists()); - assertTrue(ImportDataTest.EXISTING_FOLDER.isDirectory()); + assertTrue(Files.exists(ImportDataTest.EXISTING_FOLDER)); + assertTrue(Files.isDirectory(ImportDataTest.EXISTING_FOLDER)); - assertTrue(ImportDataTest.FILE_IN_DATABASE.exists()); - assertTrue(ImportDataTest.FILE_IN_DATABASE.isFile()); + assertTrue(Files.exists(ImportDataTest.FILE_IN_DATABASE)); + assertTrue(Files.isRegularFile(ImportDataTest.FILE_IN_DATABASE)); - assertTrue(ImportDataTest.FILE_NOT_IN_DATABASE.exists()); - assertTrue(ImportDataTest.FILE_NOT_IN_DATABASE.isFile()); - } + assertTrue(Files.exists(ImportDataTest.FILE_NOT_IN_DATABASE)); + assertTrue(Files.isRegularFile(ImportDataTest.FILE_NOT_IN_DATABASE)); - @Test - public void testOpenNotExistingDirectory() { - assertFalse(ImportDataTest.NOT_EXISTING_FOLDER.exists()); - assertFalse(ImportDataTest.NOT_EXISTING_PDF.exists()); + assertFalse(Files.exists(ImportDataTest.NOT_EXISTING_FOLDER)); + assertFalse(Files.exists(ImportDataTest.NOT_EXISTING_PDF)); } }