Skip to content

Commit

Permalink
Bring back the context menu (#5352)
Browse files Browse the repository at this point in the history
As explained in #5254 (comment) it is no longer possible to customize the context menu items, because the classes related to the text field behavior are not accessible. Thus, instead I simply copied the relevant code from openjdk. Pretty ugly workaround... Fixes #5254 and refs javafxports/openjdk-jfx#583.

Moreover, I removed the custom tab handling because it is now working as expected (i.e. #2902 is fixed in Java 9+).
  • Loading branch information
tobiasdiez authored Sep 26, 2019
1 parent a9bc912 commit d1307a8
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 75 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
51 changes: 13 additions & 38 deletions src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<List<MenuItem>> 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
Expand All @@ -74,15 +48,16 @@ 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);
this.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() {
Expand All @@ -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 {
Expand Down
41 changes: 9 additions & 32 deletions src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
}
Expand All @@ -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<List<MenuItem>> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MenuItem> 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<MenuItem> 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);
}
}
}
7 changes: 3 additions & 4 deletions src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<MenuItem>> 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);
Expand Down

0 comments on commit d1307a8

Please sign in to comment.