Requirement:
add custom menu items to the tableMenuButton (in RL done in a custom tableHeader) and those menuItems must be accessible by accelerators
Problem:
adding the menuItems is straightforward, but the accelerators are not working
Below is an example that adds a menuItem to the corner menu and - just for comparison - another to the table's contextMenu: see the latter being triggered by pressing the accelerator, while the former isn't.
What am I missing, bug or feature? Any idea how to tweak to get it working?
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
/**
* Problem: accelerator in table's corner menu not working.
*
* #author Jeanette Winzenburg, Berlin
*/
public class TableViewAccelerator extends Application {
private Parent getContent() {
TableView table = new TableView<>();
TableColumn first = new TableColumn<>("first");
table.getColumns().addAll(first);
table.setTableMenuButtonVisible(true);
Button addMenu = new Button("add MenuItem to corner");
addMenu.setOnAction(e -> {
TableViewSkin skin = (TableViewSkin) table.getSkin();
TableHeaderRow header = skin.getTableHeaderRow();
ContextMenu menu = (ContextMenu) invokeGetFieldValue(
TableHeaderRow.class,
header, "columnPopupMenu");
MenuItem item = new MenuItem("do stuff");
item.setOnAction(me -> {
LOG.info("from corner");
});
item.setAccelerator(KeyCombination.valueOf("F3"));
menu.getItems().add(item);
addMenu.setDisable(true);
});
ContextMenu menu = new ContextMenu();
MenuItem contextItem = new MenuItem("initial");
contextItem.setOnAction(e -> {
LOG.info("from initial");
});
contextItem.setAccelerator(KeyCombination.valueOf("F4"));
menu.getItems().addAll(contextItem);
table.setContextMenu(menu);
Button addToContext = new Button("add MenuItem to context");
addToContext.setOnAction(e -> {
MenuItem added = new MenuItem("added");
added.setOnAction(me -> LOG.info("from added"));
added.setAccelerator(KeyCombination.valueOf("F5"));
menu.getItems().addAll(added);
addToContext.setDisable(true);
});
BorderPane pane = new BorderPane(table);
FlowPane buttons = new FlowPane(10, 10);
buttons.getChildren().addAll(addMenu, addToContext);
pane.setBottom(buttons);
return pane;
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent(), 600, 400));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
public static Object invokeGetFieldValue(Class declaringClass, Object target, String name) {
try {
Field field = declaringClass.getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TableViewAccelerator.class.getName());
}
A couple of notes:
the registration via ControlAccelaratorSupport works perfectly
the fact that this is needed at all is unexpected but a conscious design decision, though undocumented
the support being hidden API is a bug, particularly so as it will not be accessible in fx9
Looks like corner menu items accelerators are not attached to the scene. Probably missing feature. To get it to work you can manually attach them using the ControlAcceleratorSupport class:
Button addMenu = new Button("add MenuItem to corner");
addMenu.setOnAction(e -> {
TableViewSkin skin = (TableViewSkin) table.getSkin();
TableHeaderRow header = skin.getTableHeaderRow();
ContextMenu menu = (ContextMenu) invokeGetFieldValue(
TableHeaderRow.class,
header, "columnPopupMenu");
ControlAcceleratorSupport.addAcceleratorsIntoScene(menu.getItems(), table);
[...]
Related
I want to create a custom Dialog, which just displays options (see figure 1). If the user selects one of those options, the dialog should close and return the corresponding result instantly.
So far, I can only accomplish this by adding an arbitrary ButtonType to the Dialog, hiding it by using setVisible(false) and applying fire() in the EventHandler of the clicked option.
This weird workaround actually works fine, but seems to me very unprofessional ...
So, how to do this in a more professional or proper way without using the ButtonType trick?
My workaround-code looks like this (Dialog class):
public class CustomDialog extends Dialog<String> {
private static final String[] OPTIONS
= new String[]{"Option1", "Option2", "Option3", "Option4"};
private String selectedOption = null;
Button applyButton;
public CustomDialog() {
super();
initStyle(StageStyle.DECORATED);
VBox vBox = new VBox();
for (String option : OPTIONS) {
Button optionButton = new Button(option);
optionButton.setOnAction((event) -> {
selectedOption = option;
applyButton.fire();
});
vBox.getChildren().add(optionButton);
}
getDialogPane().setContent(vBox);
getDialogPane().getButtonTypes().add(ButtonType.APPLY);
applyButton = (Button) getDialogPane().lookupButton(ButtonType.APPLY);
applyButton.setVisible(false);
setResultConverter((dialogButton) -> {
return selectedOption;
});
}
}
Using the dialog class:
CustomDialog dialog = new CustomDialog();
Optional<String> result = dialog.showAndWait();
String selected = null;
if (result.isPresent()) {
selected = result.get();
} else if (selected == null) {
System.exit(0);
}
A Dialog is just a window displaying a DialogPane, and, quoting the Javadocs for DialogPane:
DialogPane operates on the concept of ButtonType. A ButtonType is a
descriptor of a single button that should be represented visually in
the DialogPane. Developers who create a DialogPane therefore must
specify the button types that they want to display
(my emphasis). Therefore, while you've shown one possible workaround and in the other answer Slaw has shown another, if you're trying to use a Dialog without using ButtonType and its associated result converter, you're really using the Dialog class for something for which it's not intended.
The functionality you describe is perfectly achievable with a regular modal Stage. For example, the following gives the same basic behavior you describe and involves no ButtonTypes:
package org.jamesd.examples.dialog;
import java.util.Optional;
import java.util.stream.Stream;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
public class CustomDialog {
private static final String[] OPTIONS
= {"Option 1", "Option 2", "Option 3", "Option 4"};
private final Stage stage ;
private String selectedOption = null ;
public CustomDialog() {
this(null);
}
public CustomDialog(Window parent) {
var vbox = new VBox();
// Real app should use an external style sheet:
vbox.setStyle("-fx-padding: 12px; -fx-spacing: 5px;");
Stream.of(OPTIONS)
.map(this::createButton)
.forEach(vbox.getChildren()::add);
var scene = new Scene(vbox);
stage = new Stage();
stage.initOwner(parent);
stage.initModality(Modality.WINDOW_MODAL);
stage.setScene(scene);
}
private Button createButton(String text) {
var button = new Button(text);
button.setOnAction(e -> {
selectedOption = text ;
stage.close();
});
return button ;
}
public Optional<String> showDialog() {
selectedOption = null ;
stage.showAndWait();
return Optional.ofNullable(selectedOption);
}
}
Here's a simple application class which uses this custom dialog:
package org.jamesd.examples.dialog;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) throws Exception {
var root = new VBox();
// Real app should use an external style sheet:
root.setStyle("-fx-padding: 12px; -fx-spacing: 5px;");
var showDialog = new Button("Show dialog");
var label = new Label("No option chosen");
showDialog.setOnAction(e ->
new CustomDialog(stage)
.showDialog()
.ifPresentOrElse(label::setText, Platform::exit));
root.getChildren().addAll(showDialog, label);
stage.setScene(new Scene(root));
stage.show();
}
}
As pointed out by both #Sedrick and #James_D, the Dialog API is built around the concept of "button types". Not using ButtonType goes against the API and, because of this, will always seem hacky/wrong. That said, there is a slight alteration you could make to your current code that satisfies your "without using any 'ButtonType'-controls" goal. It doesn't appear to be documented, but if you set the result property manually it triggers the close-and-return-result process. This means you don't need to add any ButtonType and can bypass the resultConverter completely. Here's a proof-of-concept:
OptonsDialog.java:
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Dialog;
import javafx.scene.layout.VBox;
public class OptionsDialog<T extends OptionsDialog.Option> extends Dialog<T> {
public interface Option {
String getDisplayText();
}
#SafeVarargs
public OptionsDialog(T... options) {
if (options.length == 0) {
throw new IllegalArgumentException("must provide at least one option");
}
var content = new VBox(10);
content.setAlignment(Pos.CENTER);
content.setPadding(new Insets(15, 25, 15, 25));
for (var option : options) {
var button = new Button(option.getDisplayText());
button.setOnAction(
event -> {
event.consume();
setResult(option);
});
content.getChildren().add(button);
}
getDialogPane().setContent(content);
}
}
App.java:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.stage.Window;
public class App extends Application {
private enum Foo implements OptionsDialog.Option {
OPTION_1("Option Number 1"),
OPTION_2("Option Number 2"),
OPTION_3("Option Number 3"),
OPTION_4("Option Number 4");
private final String displayText;
Foo(String displayText) {
this.displayText = displayText;
}
#Override
public String getDisplayText() {
return displayText;
}
}
#Override
public void start(Stage primaryStage) {
var button = new Button("Click me!");
button.setOnAction(
event -> {
event.consume();
showChosenOption(primaryStage, promptUserForOption(primaryStage));
});
primaryStage.setScene(new Scene(new StackPane(button), 500, 300));
primaryStage.show();
}
private static Foo promptUserForOption(Window owner) {
var dialog = new OptionsDialog<>(Foo.values());
dialog.initOwner(owner);
dialog.setTitle("Choose Option");
return dialog.showAndWait().orElseThrow();
}
private static void showChosenOption(Window owner, OptionsDialog.Option option) {
var alert = new Alert(AlertType.INFORMATION);
alert.initOwner(owner);
alert.setHeaderText("Chosen Option");
alert.setContentText(String.format("You chose the following: \"%s\"", option.getDisplayText()));
alert.show();
}
}
It's not that different from your current workaround and it's still working against the API. This also relies on undocumented behavior (that setting the result property manually closes the dialog and returns the result). The ButtonBar at the bottom still takes up some space, though less than when you add an invisible button. It's possible, however, to get rid of this empty space by adding the following CSS:
.options-dialog-pane .button-bar {
-fx-min-height: 0;
-fx-pref-height: 0;
-fx-max-height: 0;
}
Note the above assumes you've modified the code to add the "options-dialog-pane" style class to the DialogPane used with the OptionsDialog.
I think you should read the following from the Java Docs:
Dialog Closing Rules:
It is important to understand what happens when a Dialog is closed, and also how a Dialog can be closed, especially in abnormal closing situations (such as when the 'X' button is clicked in a dialogs title bar, or when operating system specific keyboard shortcuts (such as alt-F4 on Windows) are entered). Fortunately, the outcome is well-defined in these situations, and can be best summarised in the following bullet points:
JavaFX dialogs can only be closed 'abnormally' (as defined above) in
two situations:
When the dialog only has one button, or
When the dialog has multiple buttons, as long as one of them meets one of the following requirements:
The button has a ButtonType whose ButtonBar.ButtonData is of type ButtonBar.ButtonData.CANCEL_CLOSE.
The button has a ButtonType whose ButtonBar.ButtonData returns true when ButtonBar.ButtonData.isCancelButton() is called.
In all other situations, the dialog will refuse to respond to all close requests, remaining open until the user clicks on one of the available buttons in the DialogPane area of the dialog.
If a dialog is closed abnormally, and if the dialog contains a button which meets one of the two criteria above, the dialog will attempt to set the result property to whatever value is returned from calling the result converter with the first matching ButtonType.
If for any reason the result converter returns null, or if the dialog is closed when only one non-cancel button is present, the result property will be null, and the showAndWait() method will return Optional.empty(). This later point means that, if you use either of option 2 or option 3 (as presented earlier in this class documentation), the Optional.ifPresent(java.util.function.Consumer) lambda will never be called, and code will continue executing as if the dialog had not returned any value at all.
If you don't mind the Buttons being horizontal, you should use ButtonType and setResultConverter to return a String based on which button is pressed.
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.stage.StageStyle;
import javafx.util.Callback;
/**
*
* #author blj0011
*/
public class CustomDialog extends Dialog<String>
{
String result = "";
public CustomDialog()
{
super();
initStyle(StageStyle.DECORATED);
setContentText(null);
setHeaderText(null);
ButtonType buttonOne = new ButtonType("Option1");
ButtonType buttonTwo = new ButtonType("Option2");
ButtonType buttonThree = new ButtonType("Option3");
ButtonType buttonFour = new ButtonType("Option4");
getDialogPane().getButtonTypes().addAll(buttonOne, buttonTwo, buttonThree, buttonFour);
setResultConverter(new Callback<ButtonType, String>()
{
#Override
public String call(ButtonType param)
{
if (param == buttonOne) {
return buttonOne.getText();
}
else if (param == buttonTwo) {
return buttonTwo.getText();
}
else if (param == buttonThree) {
return buttonThree.getText();
}
else if (param == buttonFour) {
return buttonFour.getText();
}
return "";
}
});
}
}
Update: As #Slaw stated in the comments, you can replace setResultConverter(...) with setResultConverter(ButtonType::getText).
Very new to JavaFX and lacking a bit of knowledge in the way controllers work but here it goes.
My problem is easy. I need to update a Label on the screen during runtime.
This problem has been addressed on this site before:
Java FX change Label text
Java FX change Label text 2
Passing Parameters
Also, are these links describing the same thing but done differently?
But my program is a little different.
The flow of the program is as follows:
The Main Stage has several Objects that extends Pane with a Label inside. These Objects can be right clicked which opens a context menu. An option in the context menu opens a new window with RadioButtons.
The idea is to select one of the RadioButtons and use that string to rewrite the Label back on the Main Stage.
However my code only works once, the first time. All subsequent changes are not shown on the screen. I can even output the Label that was changed to the Console and it shows the correct value, but never updates the Label on the Stage.
Class that has the Label on the screen:
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
public class CoursePane extends Pane {
private Label courseID;
public CoursePane(Label courseID) {
this.courseID = courseID;
}
public String getCourseID() {
return courseID.getText();
}
public Label getCourseLabel() {
return courseID;
}
public void setCourseID(String ID) {
courseID.setText(ID);
}
}
The Context Menu Class that invokes the menu:
public class CourseContext {
static String fxmlfile;
private static Object paneSrc; //the CoursePane that was clicked on
public static void start(CoursePane pane, String courseSrc) {
//Context Menu
ContextMenu contextMenu = new ContextMenu();
//MenuItems
MenuItem item4 = new MenuItem("option");
//add items to context menu
contextMenu.getItems().addAll(item4);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.isSecondaryButtonDown()) {
//the coursePane that was right clicked on
paneSrc = event.getSource().toString();
contextMenu.show(pane, event.getScreenX(), event.getScreenY());
item4.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getClassLoader().getResource("my fxml file for the radio Buttons"));
Parent root= loader.load();
ElectiveController electiveController = loader.getController();
electiveController.start( "pass the coursePane that was right clicked on" );
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setScene(scene);
stage.setTitle("Set Elective");
stage.show();
}
catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
});
}
}
And finally, the class that has the value that Label is supposed to be set to:
public class ElectiveController {
#FXML
private Button setButton;
private RadioButton chk;
//the pane that was right clicked on
private static String courseSource;
public void start(Course courseSrc) { //courseSrc: the Pane you right clicked on
courseSource = courseSrc.getCoursenamenumber().getValue();
}//end start
//sets the course pane with the selected elective radio button
#FXML
private void setElective() {
chk = (RadioButton)humElectiveGroup.getSelectedToggle();
//This is supposed to set the value for the coursePane Object to show on the screen!
MainStage.getCoursePanes().get(courseSource).setCourseID(chk.getText());
Stage stage = (Stage) setButton.getScene().getWindow();
stage.close();
}
}
I have looked into dependency injection, tried binding and passing parameters but getting the same results. I know this is straight forward, any help is appreciated! Thanks.
Here is an mcve of how you could wire up the different parts.
- It can be copy pasted into a single file and invoked.
- Note that it is not meant to represent or mock your application. It is meant to demonstrate a (very basic and simplistic) solution for the issue
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
//main class
public class UpdateViewByMenu extends Application {
private Controller controller;
#Override
public void start(Stage stage) throws Exception {
BorderPane root = new BorderPane();
controller = new Controller();
root.setTop(controller.getMenu());
root.setBottom(controller.getView());
Scene scene = new Scene(root, 350,200);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) { launch(args);}
}
//controller which "wires" view to model
class Controller {
private Model model;
private View view;
private TopMenu menu;
public Controller() {
model = new Model();
view = new View();
menu = new TopMenu();
//wire up menu to model : menu changes update model
menu.getMenuTextProperty().addListener(
e-> model.setCourseID(menu.getMenuTextProperty().get()));
//wire model to view: change in model update view
view. geLabelTextProerty().bind(model.getCourseIDProperty());
//set initial value to show
menu.getMenuTextProperty().set("Not set");
}
Model getModel() {return model;}
Pane getView() { return view;}
MenuBar getMenu() { return menu; }
}
//model which represent the data, in this case label info
class Model{
SimpleStringProperty courseIdProperty;
Model(){
courseIdProperty = new SimpleStringProperty();
}
StringProperty getCourseIDProperty() {
return courseIdProperty;
}
void setCourseID(String id) {
courseIdProperty.set(id);
}
}
//represents main view, in this case a container for a label
class View extends HBox {
private Label courseID;
View() {
courseID = new Label();
getChildren().add(courseID);
}
StringProperty geLabelTextProerty() {
return courseID.textProperty();
}
}
//menu
class TopMenu extends MenuBar{
SimpleStringProperty menuTextProperty;
TopMenu() {
menuTextProperty = new SimpleStringProperty();
Menu menu = new Menu("Select id");
MenuItem item1 = getMenuItem("10021");
MenuItem item2 = getMenuItem("10022");
MenuItem item3 = getMenuItem("10023");
MenuItem item4 = getMenuItem("10024");
menu.getItems().addAll(item1, item2, item3, item4);
getMenus().add(menu);
}
MenuItem getMenuItem(String text) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> menuTextProperty.set(item.textProperty().get()));
return item;
}
StringProperty getMenuTextProperty() {
return menuTextProperty;
}
}
Do not hesitate to ask for clarifications as needed.
i want to add and edit directly an element to a listview :
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package javafx_test;
import java.util.Observable;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.StringConverter;
/**
*
* #author karim
*/
public class Javafx_test extends Application {
#Override
public void start(Stage primaryStage) {
ObservableList<String> items = FXCollections.observableArrayList("test1", "test2");
ListView<String> list = new ListView<>(items);
list.setEditable(true);
list.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> param) {
return new TextFieldListCell<>(new StringConverter<String>() {
#Override
public String toString(String object) {
return object;
}
#Override
public String fromString(String string) {
return string;
}
});
}
});
Button btn = new Button();
btn.setText("Add String");
btn.setOnAction((ActionEvent event) -> {
String c = new String("test");
list.getItems().add(list.getItems().size(), c);
list.scrollTo(c);
list.edit(list.getItems().size() - 1);
});
VBox root = new VBox(list, btn);
Scene scene = new Scene(root);
primaryStage.setTitle("test!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Everything seems correct but that not working, it like its try to modify the first item not the newly added item in the last index, i don't know why
That's a bug.
There seems to be some truly horrible interplay between focus and editing. The basic problem seems to be that when a list cell loses focus, it cancels any editing. I think that by clicking on the button, you cause the focus to shift to that button, and then on the next rendering pulse the list cell sees it has lost focus and cancels editing. I can't quite explain why the first item in the list appears to go to an editing state, but I suspect it is due to some further interaction with the list's focusModel, which manages focus of individual items.
For a truly ugly hack, use an AnimationTimer to delay the call to ListView.edit(...) by an additional rendering frame. (In case you're not familiar with it, an AnimationTimer defines a handle(...) method that is invoked once on each rendering pulse; here I just count one frame and then call edit, and stop the timer.)
btn.setOnAction((ActionEvent event) -> {
String c = "test"+(list.getItems().size()+1);
list.getItems().add(list.getItems().size(), c);
list.scrollTo(list.getItems().size() - 1);
// list.edit(list.getItems().size() - 1);
new AnimationTimer() {
int frameCount = 0 ;
#Override
public void handle(long now) {
frameCount++ ;
if (frameCount > 1) {
list.edit(list.getItems().size() - 1);
stop();
}
}
}.start();
});
Calling scrollTo(...) with an index instead of an item seems more robust too (especially as you have items in there that are equal to each other :).)
Maybe someone else can come up with something a bit cleaner that this...
The issue seems to be the Cells not being updated before calling edit. Since updating the cells is done during layout, calling layout before starting the edit should fix the issue:
Example:
#Override
public void start(Stage primaryStage) {
ListView<String> listView = new ListView<>();
listView.setEditable(true);
listView.setCellFactory(TextFieldListCell.forListView());
Button editButton = new Button("Add & Edit");
editButton.setOnAction((ActionEvent event) -> {
listView.getItems().add("");
listView.scrollTo(listView.getItems().size() - 1);
listView.layout();
listView.edit(listView.getItems().size() - 1);
});
Scene scene = new Scene(new VBox(listView, editButton));
primaryStage.setScene(scene);
primaryStage.show();
}
What James_D already mentioned: It seems that the index for the edit method is calculated wrong. In the following example it should not grab the correct index, but it does.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ListEdit extends Application {
int i = 3;
#Override
public void start(Stage primaryStage) {
ObservableList<String> items = FXCollections.observableArrayList("test1", "test2");
ListView<String> list = new ListView<>(items);
list.setCellFactory(TextFieldListCell.forListView());
list.setEditable(true);
Button btn = new Button();
btn.setText("Add String");
btn.setOnAction((ActionEvent event) -> {
list.getItems().add(i - 1, "test" + i);
list.edit(i - 2);
i++;
});
VBox root = new VBox(list, btn);
Scene scene = new Scene(root);
primaryStage.setTitle("test!");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
How may I have a customized context menu for whole entry of the document in WebEngine javafx?
Something like this
+------------+
|Reload |
|Save page |
|Hide Images |
+------------+
I like to invoke and show this context popup for whole document entry(same for every node). Thanks.
I don't see a way to interact with the default context menu. However, it's not hard to disable it and implement your own.
Disable the default context menu with
webView.setContextMenuEnabled();
Then create your own context menu, and register a mouse listener with the web view to show it on right click:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class WebViewContextMenuTest extends Application {
private final String START_URL =
"http://stackoverflow.com/questions/27047447/customized-context-menu-on-javafx-webview-webengine/27047830#27047830";
#Override
public void start(Stage primaryStage) {
TextField locationField = new TextField(START_URL);
WebView webView = new WebView();
webView.getEngine().load(START_URL);
webView.setContextMenuEnabled(false);
createContextMenu(webView);
locationField.setOnAction(e -> {
webView.getEngine().load(getUrl(locationField.getText()));
});
BorderPane root = new BorderPane(webView, locationField, null, null, null);
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.show();
}
private void createContextMenu(WebView webView) {
ContextMenu contextMenu = new ContextMenu();
MenuItem reload = new MenuItem("Reload");
reload.setOnAction(e -> webView.getEngine().reload());
MenuItem savePage = new MenuItem("Save Page");
savePage.setOnAction(e -> System.out.println("Save page..."));
MenuItem hideImages = new MenuItem("Hide Images");
hideImages.setOnAction(e -> System.out.println("Hide Images..."));
contextMenu.getItems().addAll(reload, savePage, hideImages);
webView.setOnMousePressed(e -> {
if (e.getButton() == MouseButton.SECONDARY) {
contextMenu.show(webView, e.getScreenX(), e.getScreenY());
} else {
contextMenu.hide();
}
});
}
private String getUrl(String text) {
if (text.indexOf("://")==-1) {
return "http://" + text ;
} else {
return text ;
}
}
public static void main(String[] args) {
launch(args);
}
}
There's no easy solution for this, since there's no public API, and a request is still unresolved.
The hacky solution uses some private API, so it's not very advisable since it could change without notice.
The ContextMenu shown when the user right clicks on the web page is in another window, so using some lookups we'll try to find it, then access to its content and then modify existing or add more MenuItems.
These are the private classes required:
import com.sun.javafx.scene.control.skin.ContextMenuContent;
import com.sun.javafx.scene.control.skin.ContextMenuContent.MenuItemContainer;
In our application, we listen for a context menu request:
#Override
public void start(Stage primaryStage) {
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
Scene scene = new Scene(webView);
primaryStage.setScene(scene);
primaryStage.show();
webView.setOnContextMenuRequested(new EventHandler<ContextMenuEvent>() {
#Override
public void handle(ContextMenuEvent e) {
getPopupWindow();
}
});
}
where getPopupWindow() will:
Look for the new window being instance of ContextMenu
With lookup find the CSS selector context-menu. This is a node having as its only child a ContextMenuContent instance.
This object has an VBox as a container for all the items, which are MenuItem in an special container, MenuItemContainer.
We can access to any of the existing items, like reload page, go back, ... and customize them, modifying its text or adding a graphic.
We can add our custom items to this box, providing our own actions.
Customize the items as you need to:
private PopupWindow getPopupWindow() {
#SuppressWarnings("deprecation")
final Iterator<Window> windows = Window.impl_getWindows();
while (windows.hasNext()) {
final Window window = windows.next();
if (window instanceof ContextMenu) {
if(window.getScene()!=null && window.getScene().getRoot()!=null){
Parent root = window.getScene().getRoot();
// access to context menu content
if(root.getChildrenUnmodifiable().size()>0){
Node popup = root.getChildrenUnmodifiable().get(0);
if(popup.lookup(".context-menu")!=null){
Node bridge = popup.lookup(".context-menu");
ContextMenuContent cmc= (ContextMenuContent)((Parent)bridge).getChildrenUnmodifiable().get(0);
VBox itemsContainer = cmc.getItemsContainer();
for(Node n: itemsContainer.getChildren()){
MenuItemContainer item=(MenuItemContainer)n;
// customize text:
item.getItem().setText("My Custom: "+item.getItem().getText());
// customize graphic:
item.getItem().setGraphic(new ImageView(new Image(getClass().getResource("unlock24.png").toExternalForm())));
}
// remove some item:
// itemsContainer.getChildren().remove(0);
// adding new item:
MenuItem menuItem = new MenuItem("Save page");
menuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
System.out.println("Save Page");
}
});
// add new item:
cmc.getItemsContainer().getChildren().add(cmc.new MenuItemContainer(menuItem));
return (PopupWindow)window;
}
}
}
return null;
}
}
return null;
}
This is how it looks like:
I've tried everything. I think they made a big mistake not giving any reference to the indexed cell in anything.
I can get my menu, but not in the right place. Right click is fine.
In my TreeView I can use get KeyReleased but I don't know where to put the menu.
setOnKeyReleased((KeyEvent t) -> {
switch (t.getCode()) {
case CONTEXT_MENU:
getSelectionModel().getSelectedItem().setGraphic(new Label("hi"));
//showMenu just calls show on my ContextMenu of my subclass TreeNode
((TreeNode)getSelectionModel().getSelectedItem()).showMenu(
getSelectionModel().getSelectedItem().getGraphic().getLocalToSceneTransform());
break;
}
});
None of the layout methods will give me the coords of the TreeCell
It simply isn't possible to provide API access to the cell for a given item. Not every item has a cell associated with it. On top of that, the item which is represented by a cell may change at any time, so even if you could provide access to the cell, the API would potentially be very confusing.
The basic trick to anything like this is to create a cell factory, and register the appropriate listeners with the cell. Your case is somewhat tricky, but possible. The following works to get the cell representing the selected item (you may want to modify the code somewhat to deal with the case where the cell is scrolled off the screen).
(Note that I used the Z key, arbitrarily, as I don't have a ContextMenu key on my laptop.)
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
TreeView<String> treeView = new TreeView<>();
TreeItem<String> treeRoot = new TreeItem<>("Root");
for (int i=1; i<=5; i++) {
TreeItem<String> child = new TreeItem<>("Item "+i);
child.getChildren().addAll(new TreeItem<>("Item "+i+"A"), new TreeItem<>("Item "+i+"B"));
treeRoot.getChildren().add(child);
}
treeView.setRoot(treeRoot);
root.setCenter(treeView);
ObjectProperty<TreeCell<String>> selectedCell = new SimpleObjectProperty<>();
treeView.setCellFactory(tree -> {
TreeCell<String> cell = new TreeCell<>();
cell.textProperty().bind(cell.itemProperty());
ChangeListener<TreeItem<String>> listener = (obs, oldItem, newItem) -> {
TreeItem<String> selectedItem = treeView.getSelectionModel().getSelectedItem();
if (selectedItem == null) {
selectedCell.set(null);
} else {
if (selectedItem == cell.getTreeItem()) {
selectedCell.set(cell);
}
}
};
cell.treeItemProperty().addListener(listener);
treeView.getSelectionModel().selectedItemProperty().addListener(listener);
return cell ;
});
ContextMenu contextMenu = new ContextMenu();
for (int i=1; i<=3; i++) {
String text = "Choice "+i;
MenuItem menuItem = new MenuItem(text);
menuItem.setOnAction(event -> System.out.println(text));
contextMenu.getItems().add(menuItem);
}
treeView.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.Z) {
if (selectedCell.get() != null) {
Node anchor = selectedCell.get();
// figure center of cell in screen coords:
Bounds anchorBounds = anchor.getBoundsInParent();
double x = anchorBounds.getMinX() + anchorBounds.getWidth() / 2 ;
double y = anchorBounds.getMinY() + anchorBounds.getHeight() / 2 ;
Point2D screenLoc = anchor.getParent().localToScreen(x, y);
contextMenu.show(selectedCell.get(), screenLoc.getX(), screenLoc.getY());
}
}
});
Scene scene = new Scene(root,400,400);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}