package org.jabref.gui.filelist; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.DefaultComboBoxModel; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JProgressBar; import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.jabref.Globals; import org.jabref.gui.DialogService; import org.jabref.gui.FXDialogService; import org.jabref.gui.JabRefFrame; import org.jabref.gui.desktop.JabRefDesktop; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.externalfiletype.UnknownExternalFileType; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.util.FileHelper; import org.jabref.preferences.JabRefPreferences; import com.jgoodies.forms.builder.ButtonBarBuilder; import com.jgoodies.forms.builder.FormBuilder; import com.jgoodies.forms.layout.FormLayout; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * This class produces a dialog box for editing a single file link from a Bibtex entry. * * The information to be edited includes the file description, the link itself and the * file type. The dialog also includes convenience buttons for quick linking. * * For use when downloading files, this class also offers a progress bar and a "Downloading..." * label that can be hidden when the download is complete. */ public class FileListEntryEditor { private static final Pattern REMOTE_LINK_PATTERN = Pattern.compile("[a-z]+://.*"); private static final Log LOGGER = LogFactory.getLog(FileListEntryEditor.class); private final JTextField link = new JTextField(); private final JTextField description = new JTextField(); private final JButton ok = new JButton(Localization.lang("OK")); private final JComboBox<ExternalFileType> types; private final JProgressBar prog = new JProgressBar(SwingConstants.HORIZONTAL); private final JLabel downloadLabel = new JLabel(Localization.lang("Downloading...")); private JDialog diag; //Do not make this variable final, as then the lambda action listener will fail on compile private JabRefFrame frame; private boolean showSaveDialog; private ConfirmCloseFileListEntryEditor externalConfirm; private FileListEntry entry; //Do not make this variable final, as then the lambda action listener will fail on compiƶe private BibDatabaseContext databaseContext; private final ActionListener browsePressed = e -> { String fileText = link.getText().trim(); Optional<Path> file = FileHelper.expandFilename(this.databaseContext, fileText, Globals.prefs.getFileDirectoryPreferences()); Path workingDir = file.orElse(Paths.get(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY))); String fileName = Paths.get(fileText).getFileName().toString(); FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() .withInitialDirectory(workingDir) .withInitialFileName(fileName).build(); DialogService ds = new FXDialogService(); Optional<Path> path; if (showSaveDialog) { path = DefaultTaskExecutor.runInJavaFXThread(() -> ds.showFileSaveDialog(fileDialogConfiguration)); } else { path = DefaultTaskExecutor.runInJavaFXThread(() -> ds.showFileOpenDialog(fileDialogConfiguration)); } path.ifPresent(newFile -> { // Store the directory for next time: Globals.prefs.put(JabRefPreferences.WORKING_DIRECTORY, newFile.toString()); // If the file is below the file directory, make the path relative: List<Path> fileDirectories = this.databaseContext .getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences()); newFile = FileUtil.shortenFileName(newFile, fileDirectories); link.setText(newFile.toString()); link.requestFocus(); }); }; private boolean okPressed; private boolean okDisabledExternally; private boolean openBrowseWhenShown; private boolean dontOpenBrowseUntilDisposed; public FileListEntryEditor(JabRefFrame frame, FileListEntry entry, boolean showProgressBar, boolean showOpenButton, BibDatabaseContext databaseContext, boolean showSaveDialog) { this(frame, entry, showProgressBar, showOpenButton, databaseContext); this.showSaveDialog = showSaveDialog; } public FileListEntryEditor(JabRefFrame frame, FileListEntry entry, boolean showProgressBar, boolean showOpenButton, BibDatabaseContext databaseContext) { this.entry = entry; this.databaseContext = databaseContext; ActionListener okAction = e -> { // If OK button is disabled, ignore this event: if (!ok.isEnabled()) { return; } // If necessary, ask the external confirm object whether we are ready to close. if (externalConfirm != null) { // Construct an updated FileListEntry: storeSettings(entry); if (!externalConfirm.confirmClose(entry)) { return; } } diag.dispose(); storeSettings(FileListEntryEditor.this.entry); okPressed = true; }; types = new JComboBox<>(); types.addItemListener(itemEvent -> { if (!okDisabledExternally) { ok.setEnabled(types.getSelectedItem() != null); } }); FormLayout fileDialog = new FormLayout( "left:pref, 4dlu, fill:400dlu, 4dlu, fill:pref, 4dlu, fill:pref", "p, 8dlu, p, 8dlu, p"); FormBuilder builder = FormBuilder.create().layout(fileDialog); builder.add(Localization.lang("Link")).xy(1, 1); builder.add(link).xy(3, 1); final JButton browseBut = new JButton(Localization.lang("Browse")); browseBut.addActionListener(browsePressed); builder.add(browseBut).xy(5, 1); JButton open = new JButton(Localization.lang("Open")); if (showOpenButton) { builder.add(open).xy(7, 1); } builder.add(Localization.lang("Description")).xy(1, 3); builder.add(description).xyw(3, 3, 5); builder.getPanel().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); builder.add(Localization.lang("File type")).xy(1, 5); builder.add(types).xyw(3, 5, 5); if (showProgressBar) { builder.appendRows("2dlu, p"); builder.add(downloadLabel).xy(1, 7); builder.add(prog).xyw(3, 7, 3); } ButtonBarBuilder bb = new ButtonBarBuilder(); bb.addGlue(); bb.addRelatedGap(); bb.addButton(ok); JButton cancel = new JButton(Localization.lang("Cancel")); bb.addButton(cancel); bb.addGlue(); bb.getPanel().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); ok.addActionListener(okAction); // Add OK action to the two text fields to simplify entering: link.addActionListener(okAction); description.addActionListener(okAction); open.addActionListener(e -> openFile()); AbstractAction cancelAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { diag.dispose(); } }; cancel.addActionListener(cancelAction); // Key bindings: ActionMap am = builder.getPanel().getActionMap(); InputMap im = builder.getPanel().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); im.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close"); am.put("close", cancelAction); link.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent documentEvent) { checkExtension(); } @Override public void removeUpdate(DocumentEvent documentEvent) { // Do nothing } @Override public void changedUpdate(DocumentEvent documentEvent) { checkExtension(); } }); diag = new JDialog(frame, Localization.lang("Select files"), true); diag.getContentPane().add(builder.getPanel(), BorderLayout.CENTER); diag.getContentPane().add(bb.getPanel(), BorderLayout.SOUTH); diag.pack(); diag.setLocationRelativeTo(frame); diag.addWindowListener(new WindowAdapter() { @Override public void windowActivated(WindowEvent event) { if (openBrowseWhenShown && !dontOpenBrowseUntilDisposed) { dontOpenBrowseUntilDisposed = true; SwingUtilities.invokeLater(() -> browsePressed.actionPerformed(new ActionEvent(browseBut, 0, ""))); } } @Override public void windowClosed(WindowEvent event) { dontOpenBrowseUntilDisposed = false; } }); setValues(entry); } private void checkExtension() { if ((types.getSelectedIndex() == -1) && (!link.getText().trim().isEmpty())) { // Check if this looks like a remote link: if (FileListEntryEditor.REMOTE_LINK_PATTERN.matcher(link.getText()).matches()) { Optional<ExternalFileType> type = ExternalFileTypes.getInstance().getExternalFileTypeByExt("html"); if (type.isPresent()) { types.setSelectedItem(type.get()); return; } } // Try to guess the file type: String theLink = link.getText().trim(); ExternalFileTypes.getInstance().getExternalFileTypeForName(theLink).ifPresent(types::setSelectedItem); } } private void openFile() { ExternalFileType type = (ExternalFileType) types.getSelectedItem(); if (type != null) { try { JabRefDesktop.openExternalFileAnyFormat(databaseContext, link.getText(), Optional.of(type)); } catch (IOException e) { LOGGER.error("File could not be opened", e); } } } public void setExternalConfirm(ConfirmCloseFileListEntryEditor eC) { this.externalConfirm = eC; } public void setOkEnabled(boolean enabled) { okDisabledExternally = !enabled; ok.setEnabled(enabled); } public JProgressBar getProgressBar() { return prog; } public JLabel getProgressBarLabel() { return downloadLabel; } public void setEntry(FileListEntry entry) { this.entry = entry; setValues(entry); } public void setVisible(boolean visible, boolean openBrowse) { openBrowseWhenShown = openBrowse && Globals.prefs.getBoolean(JabRefPreferences.ALLOW_FILE_AUTO_OPEN_BROWSE); if (visible) { okPressed = false; } String title; if (showSaveDialog) { title = Localization.lang("Save file"); } else { title = Localization.lang("Select files"); } diag.setTitle(title); diag.setVisible(visible); } public boolean isVisible() { return (diag != null) && diag.isVisible(); } private void setValues(FileListEntry entry) { description.setText(entry.getDescription()); link.setText(entry.getLink()); Collection<ExternalFileType> list = ExternalFileTypes.getInstance().getExternalFileTypeSelection(); types.setModel(new DefaultComboBoxModel<>(list.toArray(new ExternalFileType[list.size()]))); types.setSelectedIndex(-1); // See what is a reasonable selection for the type combobox: if ((entry.getType().isPresent()) && !(entry.getType().get() instanceof UnknownExternalFileType)) { types.setSelectedItem(entry.getType().get()); } else if ((entry.getLink() != null) && (!entry.getLink().isEmpty())) { checkExtension(); } } private void storeSettings(FileListEntry listEntry) { String descriptionText = this.description.getText().trim(); String fileLink = ""; // See if we should trim the file link to be relative to the file directory: try { List<String> dirs = databaseContext.getFileDirectories(Globals.prefs.getFileDirectoryPreferences()); if (dirs.isEmpty()) { fileLink = this.link.getText().trim(); } else { boolean found = false; for (String dir : dirs) { String canPath = (new File(dir)).getCanonicalPath(); File fl = new File(this.link.getText().trim()); if (fl.isAbsolute()) { String flPath = fl.getCanonicalPath(); if ((flPath.length() > canPath.length()) && (flPath.startsWith(canPath))) { fileLink = fl.getCanonicalPath().substring(canPath.length() + 1); found = true; break; } } } if (!found) { fileLink = this.link.getText().trim(); } } } catch (IOException ex) { // Don't think this should happen, but set the file link directly as a fallback: fileLink = this.link.getText().trim(); } ExternalFileType type = (ExternalFileType) types.getSelectedItem(); listEntry.setDescription(descriptionText); listEntry.setType(Optional.ofNullable(type)); listEntry.setLink(fileLink); } public boolean okPressed() { return okPressed; } }