Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring back the context menu #5352

Merged
merged 2 commits into from
Sep 26, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `#
- 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)

### 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