package org.jabref.gui.entryeditor; import java.awt.AWTKeyStroke; import java.awt.Component; import java.awt.KeyboardFocusManager; import java.awt.event.FocusListener; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.KeyStroke; import javafx.embed.swing.JFXPanel; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.RowConstraints; import org.jabref.Globals; import org.jabref.gui.BasePanel; import org.jabref.gui.FXDialogService; import org.jabref.gui.GUIGlobals; import org.jabref.gui.JabRefFrame; import org.jabref.gui.autocompleter.AutoCompleteListener; import org.jabref.gui.fieldeditors.FieldEditor; import org.jabref.gui.fieldeditors.FieldEditorFX; import org.jabref.gui.fieldeditors.FieldEditors; import org.jabref.gui.fieldeditors.FieldNameLabel; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; import org.jabref.model.entry.FieldProperty; import org.jabref.model.entry.InternalBibtexFields; /** * A single tab displayed in the EntryEditor holding several FieldEditors. */ class EntryEditorTab { private final JFXPanel panel = new JFXPanel(); private final List<String> fields; private final EntryEditor parent; private final Map<String, FieldEditorFX> editors = new LinkedHashMap<>(); private final FocusListener fieldListener = new EntryEditorTabFocusListener(this); private final String tabTitle; private final JabRefFrame frame; private final BasePanel basePanel; private FieldEditorFX activeField; private BibEntry entry; private boolean updating; public EntryEditorTab(JabRefFrame frame, BasePanel basePanel, List<String> fields, EntryEditor parent, boolean addKeyField, boolean compressed, String tabTitle, BibEntry entry) { this.entry = Objects.requireNonNull(entry); if (fields == null) { this.fields = new ArrayList<>(); } else { this.fields = new ArrayList<>(fields); } // Add the edit field for Bibtex-key. if (addKeyField) { this.fields.add(BibEntry.KEY_FIELD); } this.parent = parent; this.tabTitle = tabTitle; this.frame = frame; this.basePanel = basePanel; // Execute on JavaFX Application Thread DefaultTaskExecutor.runInJavaFXThread(() -> { Region root = setupPanel(frame, basePanel, addKeyField, compressed, tabTitle); if (GUIGlobals.currentFont != null) { root.setStyle( "text-area-background: " + convertToHex(GUIGlobals.validFieldBackgroundColor) + ";" + "text-area-foreground: " + convertToHex(GUIGlobals.editorTextColor) + ";" + "text-area-highlight: " + convertToHex(GUIGlobals.activeBackgroundColor) + ";" ); } root.getStylesheets().add("org/jabref/gui/entryeditor/EntryEditor.css"); panel.setScene(new Scene(root)); }); // The following line makes sure focus cycles inside tab instead of being lost to other parts of the frame: panel.setFocusCycleRoot(true); } private static void addColumn(GridPane gridPane, int columnIndex, List<Label> nodes) { gridPane.addColumn(columnIndex, nodes.toArray(new Node[nodes.size()])); } private static void addColumn(GridPane gridPane, int columnIndex, Stream<Parent> nodes) { gridPane.addColumn(columnIndex, nodes.toArray(Node[]::new)); } private String convertToHex(java.awt.Color color) { return String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()); } private Region setupPanel(JabRefFrame frame, BasePanel bPanel, boolean addKeyField, boolean compressed, String title) { setupKeyBindings(panel.getInputMap(JComponent.WHEN_FOCUSED), panel.getActionMap()); panel.setName(title); editors.clear(); List<Label> labels = new ArrayList<>(); for (String fieldName : fields) { // TODO: Reenable/migrate this // Store the editor for later reference: /* FieldEditor fieldEditor; int defaultHeight; int wHeight = (int) (50.0 * InternalBibtexFields.getFieldWeight(field)); if (InternalBibtexFields.getFieldProperties(field).contains(FieldProperty.SINGLE_ENTRY_LINK)) { fieldEditor = new EntryLinkListEditor(frame, bPanel.getBibDatabaseContext(), field, null, parent, true); defaultHeight = 0; } else if (InternalBibtexFields.getFieldProperties(field).contains(FieldProperty.MULTIPLE_ENTRY_LINK)) { fieldEditor = new EntryLinkListEditor(frame, bPanel.getBibDatabaseContext(), field, null, parent, false); defaultHeight = 0; } else { fieldEditor = new TextArea(field, null, getPrompt(field)); //parent.addSearchListener((TextArea) fieldEditor); defaultHeight = fieldEditor.getPane().getPreferredSize().height; } Optional<JComponent> extra = parent.getExtra(fieldEditor); // Add autocompleter listener, if required for this field: /* AutoCompleter<String> autoCompleter = bPanel.getAutoCompleters().get(field); AutoCompleteListener autoCompleteListener = null; if (autoCompleter != null) { autoCompleteListener = new AutoCompleteListener(autoCompleter); } setupJTextComponent(fieldEditor.getTextComponent(), autoCompleteListener); fieldEditor.setAutoCompleteListener(autoCompleteListener); */ FieldEditorFX fieldEditor = FieldEditors.getForField(fieldName, Globals.taskExecutor, new FXDialogService(), Globals.journalAbbreviationLoader, Globals.prefs.getJournalAbbreviationPreferences(), Globals.prefs, bPanel.getBibDatabaseContext(), entry.getType()); editors.put(fieldName, fieldEditor); /* // TODO: Reenable this if (i == 0) { activeField = fieldEditor; } */ /* // TODO: Reenable this if (!compressed) { fieldEditor.getPane().setPreferredSize(new Dimension(100, Math.max(defaultHeight, wHeight))); } */ /* // TODO: Reenable content selector if (!panel.getBibDatabaseContext().getMetaData().getContentSelectorValuesForField(editor.getFieldName()).isEmpty()) { FieldContentSelector ws = new FieldContentSelector(frame, panel, frame, editor, storeFieldAction, false, ", "); contentSelectors.add(ws); controls.add(ws, BorderLayout.NORTH); } //} else if (!panel.getBibDatabaseContext().getMetaData().getContentSelectorValuesForField(fieldName).isEmpty()) { //return FieldExtraComponents.getSelectorExtraComponent(frame, panel, editor, contentSelectors, storeFieldAction); */ labels.add(new FieldNameLabel(fieldName)); } GridPane gridPane = new GridPane(); gridPane.setPrefSize(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); gridPane.setMaxSize(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); gridPane.getStyleClass().add("editorPane"); ColumnConstraints columnExpand = new ColumnConstraints(); columnExpand.setHgrow(Priority.ALWAYS); ColumnConstraints columnDoNotContract = new ColumnConstraints(); columnDoNotContract.setMinWidth(Region.USE_PREF_SIZE); int rows; if (compressed) { rows = (int) Math.ceil((double) fields.size() / 2); addColumn(gridPane, 0, labels.subList(0, rows)); addColumn(gridPane, 3, labels.subList(rows, labels.size())); addColumn(gridPane, 1, editors.values().stream().map(FieldEditorFX::getNode).limit(rows)); addColumn(gridPane, 4, editors.values().stream().map(FieldEditorFX::getNode).skip(rows)); gridPane.getColumnConstraints().addAll(columnDoNotContract, columnExpand, new ColumnConstraints(10), columnDoNotContract, columnExpand); } else { rows = fields.size(); addColumn(gridPane, 0, labels); addColumn(gridPane, 1, editors.values().stream().map(FieldEditorFX::getNode)); gridPane.getColumnConstraints().addAll(columnDoNotContract, columnExpand); } RowConstraints rowExpand = new RowConstraints(); rowExpand.setVgrow(Priority.ALWAYS); rowExpand.setPercentHeight(100 / rows); for (int i = 0; i < rows; i++) { gridPane.getRowConstraints().add(rowExpand); } return gridPane; } private String getPrompt(String field) { Set<FieldProperty> fieldProperties = InternalBibtexFields.getFieldProperties(field); if (fieldProperties.contains(FieldProperty.PERSON_NAMES)) { return String.format("%1$s and %1$s and others", Localization.lang("Firstname Lastname")); } else if (fieldProperties.contains(FieldProperty.DOI)) { return "10.ORGANISATION/ID"; } else if (fieldProperties.contains(FieldProperty.DATE)) { return "YYYY-MM-DD"; } switch (field) { case FieldName.YEAR: return "YYYY"; case FieldName.MONTH: return "MM or #mmm#"; case FieldName.URL: return "https://"; } return ""; } private BibEntry getEntry() { return entry; } public void setEntry(BibEntry entry) { try { updating = true; for (FieldEditorFX editor : editors.values()) { DefaultTaskExecutor.runInJavaFXThread(() -> editor.bindToEntry(entry)); } this.entry = entry; } finally { updating = false; } } private boolean isFieldModified(FieldEditor fieldEditor) { String text = fieldEditor.getText().trim(); if (text.isEmpty()) { return getEntry().hasField(fieldEditor.getFieldName()); } else { return !Optional.of(text).equals(getEntry().getField(fieldEditor.getFieldName())); } } public void markIfModified(FieldEditor fieldEditor) { // Only mark as changed if not already is and the field was indeed modified if (!updating && !basePanel.isModified() && isFieldModified(fieldEditor)) { markBaseChanged(); } } private void markBaseChanged() { basePanel.markBaseChanged(); } /** * Only sets the activeField variable but does not focus it. * <p> * If you want to focus it call {@link #focus()} afterwards. */ // TODO: Reenable or delete this //public void setActive(FieldEditor fieldEditor) { // activeField = fieldEditor; //} //public FieldEditor getActive() { // return activeField; //} public void setActive(String fieldName) { if (editors.containsKey(fieldName)) { activeField = editors.get(fieldName); } } public List<String> getFields() { return Collections.unmodifiableList(fields); } public void focus() { if (activeField != null) { activeField.requestFocus(); } } /** * Reset all fields from the data in the BibEntry. */ public void updateAll() { setEntry(getEntry()); } public boolean updateField(String field, String content) { if (!editors.containsKey(field)) { return false; } // TODO: Reenable or probably better delete this /* FieldEditor fieldEditor = editors.get(field); if (fieldEditor.getText().equals(content)) { return true; } // trying to preserve current edit position (fixes SF bug #1285) if (fieldEditor.getTextComponent() instanceof JTextComponent) { int initialCaretPosition = ((JTextComponent) fieldEditor).getCaretPosition(); fieldEditor.setText(content); int textLength = fieldEditor.getText().length(); if (initialCaretPosition < textLength) { ((JTextComponent) fieldEditor).setCaretPosition(initialCaretPosition); } else { ((JTextComponent) fieldEditor).setCaretPosition(textLength); } } else { fieldEditor.setText(content); } */ return true; } public void setEnabled(boolean enabled) { /* // TODO: Reenable this for (FieldEditor editor : editors.values()) { editor.setEnabled(enabled); } */ } public Component getPane() { return panel; } public EntryEditor getParent() { return parent; } public String getTabTitle() { return tabTitle; } private void setupKeyBindings(final InputMap inputMap, final ActionMap actionMap) { inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.ENTRY_EDITOR_PREVIOUS_ENTRY), "prev"); actionMap.put("prev", parent.getPrevEntryAction()); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.ENTRY_EDITOR_NEXT_ENTRY), "next"); actionMap.put("next", parent.getNextEntryAction()); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.ENTRY_EDITOR_STORE_FIELD), "store"); actionMap.put("store", parent.getStoreFieldAction()); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.ENTRY_EDITOR_NEXT_PANEL), "right"); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.ENTRY_EDITOR_NEXT_PANEL_2), "right"); actionMap.put("left", parent.getSwitchLeftAction()); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.ENTRY_EDITOR_PREVIOUS_PANEL), "left"); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.ENTRY_EDITOR_PREVIOUS_PANEL_2), "left"); actionMap.put("right", parent.getSwitchRightAction()); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.HELP), "help"); actionMap.put("help", parent.getHelpAction()); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.SAVE_DATABASE), "save"); actionMap.put("save", parent.getSaveDatabaseAction()); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.NEXT_TAB), "nexttab"); actionMap.put("nexttab", this.frame.nextTab); inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.PREVIOUS_TAB), "prevtab"); actionMap.put("prevtab", this.frame.prevTab); } /** * Set up key bindings and focus listener for the FieldEditor. * * @param component */ private void setupJTextComponent(final JComponent component, final AutoCompleteListener autoCompleteListener) { // Here we add focus listeners to the component. The funny code is because we need // to guarantee that the AutoCompleteListener - if used - is called before fieldListener // on a focus lost event. The AutoCompleteListener is responsible for removing any // current suggestion when focus is lost, and this must be done before fieldListener // stores the current edit. Swing doesn't guarantee the order of execution of event // listeners, so we handle this by only adding the AutoCompleteListener and telling // it to call fieldListener afterwards. If no AutoCompleteListener is used, we // add the fieldListener normally. if (autoCompleteListener == null) { component.addFocusListener(fieldListener); } else { component.addKeyListener(autoCompleteListener); component.addFocusListener(autoCompleteListener); autoCompleteListener.setNextFocusListener(fieldListener); } setupKeyBindings(component.getInputMap(JComponent.WHEN_FOCUSED), component.getActionMap()); Set<AWTKeyStroke> keys = new HashSet<>( component.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)); keys.clear(); keys.add(AWTKeyStroke.getAWTKeyStroke("pressed TAB")); component.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, keys); keys = new HashSet<>(component.getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)); keys.clear(); keys.add(KeyStroke.getKeyStroke("shift pressed TAB")); component.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, keys); } }