diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb4d4a4de0..abb9743e802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +26,10 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `# - We fixed an error where the number of matched entries shown in the group pane was not updated correctly. [#4441](https://github.com/JabRef/jabref/issues/4441) - We fixed an error mentioning "javafx.controls/com.sun.javafx.scene.control" that was thrown when interacting with the toolbar. - We fixed an error where a cleared search was restored after switching libraries. [#4846](https://github.com/JabRef/jabref/issues/4846) +- We fixed an exception which occured when trying to open a non existing file from the "Recent files"-menu [#5334](https://github.com/JabRef/jabref/issues/5334) +- The context menu for fields in the entry editor is back. [#5254](https://github.com/JabRef/jabref/issues/5254) - We fixed an exception which occurred when trying to open a non existing file from the "Recent files"-menu [#5334](https://github.com/JabRef/jabref/issues/5334) - ### Removed diff --git a/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java b/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java index 846a6e2a71f..65a55854974 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java +++ b/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java @@ -7,16 +7,14 @@ import java.util.function.Supplier; import javafx.fxml.Initializable; +import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; -import javafx.scene.control.skin.TextAreaSkin; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -// TODO: TextAreaSkin changed in Java 9 public class EditorTextArea extends javafx.scene.control.TextArea implements Initializable, ContextMenuAddable { + private final ContextMenu contextMenu = new ContextMenu(); /** - * Variable that contains user-defined behavior for paste action. + * Variable that contains user-defined behavior for paste action. */ private PasteActionHandler pasteActionHandler = () -> { // Set empty paste behavior by default @@ -31,40 +29,16 @@ public EditorTextArea(final String text) { // Hide horizontal scrollbar and always wrap text setWrapText(true); - - // Should behave as a normal text field with respect to TAB behaviour - addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getCode() == KeyCode.TAB) { - // TODO: temporarily removed, as this is internal API -// TextAreaSkin skin = (TextAreaSkin) getSkin(); -// if (event.isShiftDown()) { -// // Shift + Tab > previous text area -// skin.getBehavior().traversePrevious(); -// } else { -// if (event.isControlDown()) { -// // Ctrl + Tab > insert tab -// skin.getBehavior().callAction("InsertTab"); -// } else { -// // Tab > next text area -// skin.getBehavior().traverseNext(); -// } -// } - event.consume(); - } - }); } @Override public void addToContextMenu(final Supplier> items) { - TextAreaSkin customContextSkin = new TextAreaSkin(this) { - // TODO: temporarily removed, internal API -// @Override -// public void populateContextMenu(ContextMenu contextMenu) { -// super.populateContextMenu(contextMenu); -// contextMenu.getItems().addAll(0, items.get()); -// } - }; - setSkin(customContextSkin); + setOnContextMenuRequested(event -> { + contextMenu.getItems().setAll(TextInputControlBehavior.getDefaultContextMenuItems(this)); + contextMenu.getItems().addAll(0, items.get()); + + TextInputControlBehavior.showContextMenu(this, contextMenu, event); + }); } @Override @@ -74,7 +48,8 @@ public void initialize(URL location, ResourceBundle resources) { /** * Set pasteActionHandler variable to passed handler - * @param handler an instance of PasteActionHandler that describes paste behavior + * + * @param handler an instance of PasteActionHandler that describes paste behavior */ public void setPasteActionHandler(PasteActionHandler handler) { Objects.requireNonNull(handler); @@ -82,7 +57,7 @@ public void setPasteActionHandler(PasteActionHandler handler) { } /** - * Override javafx TextArea method applying TextArea.paste() and pasteActionHandler after + * Override javafx TextArea method applying TextArea.paste() and pasteActionHandler after */ @Override public void paste() { @@ -91,7 +66,7 @@ public void paste() { } /** - * Interface presents user-described paste behaviour applying to paste method + * Interface presents user-described paste behaviour applying to paste method */ @FunctionalInterface public interface PasteActionHandler { diff --git a/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java b/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java index d880f665529..a10583e2774 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java +++ b/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java @@ -6,16 +6,15 @@ import java.util.function.Supplier; import javafx.fxml.Initializable; +import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; -import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; -//import com.sun.javafx.scene.control.skin.TextFieldSkin; - -// TODO: TextFieldSkin changed in Java 9 public class EditorTextField extends javafx.scene.control.TextField implements Initializable, ContextMenuAddable { + private final ContextMenu contextMenu = new ContextMenu(); + public EditorTextField() { this(""); } @@ -26,38 +25,16 @@ public EditorTextField(final String text) { // Always fill out all the available space setPrefHeight(Double.POSITIVE_INFINITY); HBox.setHgrow(this, Priority.ALWAYS); - - // Should behave as a normal text field with respect to TAB behaviour - addEventFilter(KeyEvent.KEY_PRESSED, event -> { -// if (event.getCode() == KeyCode.TAB) { -// TextFieldSkin skin = (TextFieldSkin) getSkin(); -// if (event.isShiftDown()) { -// // Shift + Tab > previous text area -// skin.getBehavior().traversePrevious(); -// } else { -// if (event.isControlDown()) { -// // Ctrl + Tab > insert tab -// skin.getBehavior().callAction("InsertTab"); -// } else { -// // Tab > next text area -// skin.getBehavior().traverseNext(); -// } -// } -// event.consume(); -// } - }); } @Override public void addToContextMenu(final Supplier> items) { -// TextFieldSkin customContextSkin = new TextFieldSkin(this) { -// @Override -// public void populateContextMenu(ContextMenu contextMenu) { -// super.populateContextMenu(contextMenu); -// contextMenu.getItems().addAll(0, items.get()); -// } -// }; -// setSkin(customContextSkin); + setOnContextMenuRequested(event -> { + contextMenu.getItems().setAll(TextInputControlBehavior.getDefaultContextMenuItems(this)); + contextMenu.getItems().addAll(0, items.get()); + + TextInputControlBehavior.showContextMenu(this, contextMenu, event); + }); } @Override diff --git a/src/main/java/org/jabref/gui/fieldeditors/TextInputControlBehavior.java b/src/main/java/org/jabref/gui/fieldeditors/TextInputControlBehavior.java new file mode 100644 index 00000000000..bbe745e8de9 --- /dev/null +++ b/src/main/java/org/jabref/gui/fieldeditors/TextInputControlBehavior.java @@ -0,0 +1,206 @@ +package org.jabref.gui.fieldeditors; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javafx.geometry.Point2D; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.IndexRange; +import javafx.scene.control.MenuItem; +import javafx.scene.control.PasswordField; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; +import javafx.scene.control.skin.TextAreaSkin; +import javafx.scene.control.skin.TextFieldSkin; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ContextMenuEvent; +import javafx.stage.Screen; +import javafx.stage.Window; + +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.OS; + +import com.sun.javafx.scene.control.Properties; + +/** + * This class contains some code taken from {@link com.sun.javafx.scene.control.behavior.TextInputControlBehavior}, + * witch is not accessible and thus we have no other choice. + * TODO: remove this ugly workaround as soon as control behavior is made public + * reported at https://github.com/javafxports/openjdk-jfx/issues/583 + */ +public class TextInputControlBehavior { + + private static final boolean SHOW_HANDLES = Properties.IS_TOUCH_SUPPORTED && !OS.OS_X; + + /** + * Returns the default context menu items (except undo/redo) + */ + public static List getDefaultContextMenuItems(TextInputControl textInputControl) { + boolean editable = textInputControl.isEditable(); + boolean hasText = (textInputControl.getLength() > 0); + boolean hasSelection = (textInputControl.getSelection().getLength() > 0); + boolean allSelected = (textInputControl.getSelection().getLength() == textInputControl.getLength()); + boolean maskText = (textInputControl instanceof PasswordField); // (maskText("A") != "A"); + ArrayList items = new ArrayList<>(); + + MenuItem cutMI = new MenuItem(Localization.lang("Cut")); + cutMI.setOnAction(e -> textInputControl.cut()); + MenuItem copyMI = new MenuItem(Localization.lang("Copy")); + copyMI.setOnAction(e -> textInputControl.copy()); + MenuItem pasteMI = new MenuItem(Localization.lang("Paste")); + pasteMI.setOnAction(e -> textInputControl.paste()); + MenuItem deleteMI = new MenuItem(Localization.lang("Delete")); + deleteMI.setOnAction(e -> { + IndexRange selection = textInputControl.getSelection(); + textInputControl.deleteText(selection); + }); + MenuItem selectAllMI = new MenuItem(Localization.lang("Select all")); + selectAllMI.setOnAction(e -> textInputControl.selectAll()); + MenuItem separatorMI = new SeparatorMenuItem(); + + if (SHOW_HANDLES) { + if (!maskText && hasSelection) { + if (editable) { + items.add(cutMI); + } + items.add(copyMI); + } + if (editable && Clipboard.getSystemClipboard().hasString()) { + items.add(pasteMI); + } + if (hasText && !allSelected) { + items.add(selectAllMI); + } + selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE); + } else { + if (editable) { + items.addAll(Arrays.asList(cutMI, copyMI, pasteMI, deleteMI, separatorMI, selectAllMI)); + } else { + items.addAll(Arrays.asList(copyMI, separatorMI, selectAllMI)); + } + cutMI.setDisable(maskText || !hasSelection); + copyMI.setDisable(maskText || !hasSelection); + pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString()); + deleteMI.setDisable(!hasSelection); + } + + return items; + } + + /** + * @implNote taken from {@link com.sun.javafx.scene.control.behavior.TextFieldBehavior#contextMenuRequested(javafx.scene.input.ContextMenuEvent)} + */ + public static void showContextMenu(TextField textField, ContextMenu contextMenu, ContextMenuEvent e) { + double screenX = e.getScreenX(); + double screenY = e.getScreenY(); + double sceneX = e.getSceneX(); + + TextFieldSkin skin = (TextFieldSkin) textField.getSkin(); + + if (Properties.IS_TOUCH_SUPPORTED) { + Point2D menuPos; + if (textField.getSelection().getLength() == 0) { + skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false); + menuPos = skin.getMenuPosition(); + } else { + menuPos = skin.getMenuPosition(); + if (menuPos != null && (menuPos.getX() <= 0 || menuPos.getY() <= 0)) { + skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false); + menuPos = skin.getMenuPosition(); + } + } + + if (menuPos != null) { + Point2D p = textField.localToScene(menuPos); + Scene scene = textField.getScene(); + Window window = scene.getWindow(); + Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(), + window.getY() + scene.getY() + p.getY()); + screenX = location.getX(); + sceneX = p.getX(); + screenY = location.getY(); + } + } + + double menuWidth = contextMenu.prefWidth(-1); + double menuX = screenX - (Properties.IS_TOUCH_SUPPORTED ? (menuWidth / 2) : 0); + Screen currentScreen = Screen.getPrimary(); + Rectangle2D bounds = currentScreen.getBounds(); + + if (menuX < bounds.getMinX()) { + textField.getProperties().put("CONTEXT_MENU_SCREEN_X", screenX); + textField.getProperties().put("CONTEXT_MENU_SCENE_X", sceneX); + contextMenu.show(textField, bounds.getMinX(), screenY); + } else if (screenX + menuWidth > bounds.getMaxX()) { + double leftOver = menuWidth - (bounds.getMaxX() - screenX); + textField.getProperties().put("CONTEXT_MENU_SCREEN_X", screenX); + textField.getProperties().put("CONTEXT_MENU_SCENE_X", sceneX); + contextMenu.show(textField, screenX - leftOver, screenY); + } else { + textField.getProperties().put("CONTEXT_MENU_SCREEN_X", 0); + textField.getProperties().put("CONTEXT_MENU_SCENE_X", 0); + contextMenu.show(textField, menuX, screenY); + } + } + + /** + * @implNote taken from {@link com.sun.javafx.scene.control.behavior.TextAreaBehavior#contextMenuRequested(javafx.scene.input.ContextMenuEvent)} + */ + public static void showContextMenu(TextArea textArea, ContextMenu contextMenu, ContextMenuEvent e) { + double screenX = e.getScreenX(); + double screenY = e.getScreenY(); + double sceneX = e.getSceneX(); + + TextAreaSkin skin = (TextAreaSkin) textArea.getSkin(); + + if (Properties.IS_TOUCH_SUPPORTED) { + Point2D menuPos; + if (textArea.getSelection().getLength() == 0) { + skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false); + menuPos = skin.getMenuPosition(); + } else { + menuPos = skin.getMenuPosition(); + if (menuPos != null && (menuPos.getX() <= 0 || menuPos.getY() <= 0)) { + skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false); + menuPos = skin.getMenuPosition(); + } + } + + if (menuPos != null) { + Point2D p = textArea.localToScene(menuPos); + Scene scene = textArea.getScene(); + Window window = scene.getWindow(); + Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(), + window.getY() + scene.getY() + p.getY()); + screenX = location.getX(); + sceneX = p.getX(); + screenY = location.getY(); + } + } + + double menuWidth = contextMenu.prefWidth(-1); + double menuX = screenX - (Properties.IS_TOUCH_SUPPORTED ? (menuWidth / 2) : 0); + Screen currentScreen = Screen.getPrimary(); + Rectangle2D bounds = currentScreen.getBounds(); + + if (menuX < bounds.getMinX()) { + textArea.getProperties().put("CONTEXT_MENU_SCREEN_X", screenX); + textArea.getProperties().put("CONTEXT_MENU_SCENE_X", sceneX); + contextMenu.show(textArea, bounds.getMinX(), screenY); + } else if (screenX + menuWidth > bounds.getMaxX()) { + double leftOver = menuWidth - (bounds.getMaxX() - screenX); + textArea.getProperties().put("CONTEXT_MENU_SCREEN_X", screenX); + textArea.getProperties().put("CONTEXT_MENU_SCENE_X", sceneX); + contextMenu.show(textArea, screenX - leftOver, screenY); + } else { + textArea.getProperties().put("CONTEXT_MENU_SCREEN_X", 0); + textArea.getProperties().put("CONTEXT_MENU_SCENE_X", 0); + contextMenu.show(textArea, menuX, screenY); + } + } +} diff --git a/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java b/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java index 6e5d6012761..08d22698a8c 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java @@ -30,16 +30,15 @@ public UrlEditor(Field field, DialogService dialogService, AutoCompleteSuggestio this.viewModel = new UrlEditorViewModel(field, suggestionProvider, dialogService, fieldCheckers); ViewLoader.view(this) - .root(this) - .load(); + .root(this) + .load(); textArea.textProperty().bindBidirectional(viewModel.textProperty()); Supplier> contextMenuSupplier = EditorMenus.getCleanupURLMenu(textArea); textArea.addToContextMenu(contextMenuSupplier); // init paste handler for URLEditor to format pasted url link in textArea - textArea.setPasteActionHandler(()-> - textArea.setText(new CleanupURLFormatter().format(new TrimWhitespaceFormatter().format(textArea.getText())))); + textArea.setPasteActionHandler(() -> textArea.setText(new CleanupURLFormatter().format(new TrimWhitespaceFormatter().format(textArea.getText())))); new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textArea);