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:
Related
How can I make a custom Event that triggers on Stage.setScene()?
In my code, the button switches the Scenes and that works fine. However, I would like to extend the Stage to have an additional Event that is triggered when a button or possibly any other Element triggers a setScene.
Example:
package sample;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) {
Group g1 = new Group();
Button b1 = new Button("2");
g1.getChildren().setAll(b1);
Scene scene1 = new Scene(g1, 50, 50);
Group g2 = new Group();
Button b2 = new Button("1");
g2.getChildren().setAll(b2);
Scene scene2 = new Scene(g2, 50, 50);
stage.setScene(scene1);
stage.setTitle("JavaFX Application Life Cycle");
b1.setOnAction(actionEvent -> {
System.out.println("1");
stage.setScene(scene2);
});
b2.setOnAction(actionEvent -> {
System.out.println("2");
stage.setScene(scene1);
});
stage.show();
}
}
You can add a ChangeListener<Scene> to your Stage like this:
stage.sceneProperty().addListener((observable, oldScene, newScene) -> {
System.out.println("New scene: " + newScene);
System.out.println("Old scene: " + oldScene);
});
I believe using a listener, as shown in the answer by #M.S., is probably the best and simplest way to react to scene changes. However, you ask about how to make a "custom event" that you can fire when the scene changes; by "event" I assume you mean a subclass of javafx.event.Event. So while I recommend sticking with a simple listener, here's an example of a custom event.
First, you need a custom event class:
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.Scene;
import javafx.stage.Window;
public class SceneChangedEvent extends Event {
public static final EventType<SceneChangedEvent> SCENE_CHANGED =
new EventType<>(Event.ANY, "SCENE_CHANGED");
public static final EventType<SceneChangedEvent> ANY = SCENE_CHANGED;
private transient Window window;
private transient Scene oldScene;
private transient Scene newScene;
public SceneChangedEvent(Window window, Scene oldScene, Scene newScene) {
super(window, window, SCENE_CHANGED);
this.window = window;
this.oldScene = oldScene;
this.newScene = newScene;
}
public Window getWindow() {
return window;
}
public Scene getOldScene() {
return oldScene;
}
public Scene getNewScene() {
return newScene;
}
}
I'm not sure what information you want to carry with the event so I just added the source Window as well as the old and new Scenes. If you're wondering about the ANY = SCENE_CHANGED, I'm just following the pattern used by javafx.event.ActionEvent (which also only has a single event-type).
Then you simply need to fire the event when the scene changes. To implement this you're still going to need a change listener. As you mention wanting to extend Stage here's an example of that:
import javafx.beans.NamedArg;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class CustomStage extends Stage {
private final ObjectProperty<EventHandler<? super SceneChangedEvent>> onSceneChanged =
new SimpleObjectProperty<>(this, "onSceneChanged") {
#Override
protected void invalidated() {
setEventHandler(SceneChangedEvent.SCENE_CHANGED, get());
}
};
public final void setOnSceneChanged(EventHandler<? super SceneChangedEvent> handler) {
onSceneChanged.set(handler);
}
public final EventHandler<? super SceneChangedEvent> getOnSceneChanged() {
return onSceneChanged.get();
}
public final ObjectProperty<EventHandler<? super SceneChangedEvent>> onSceneChangedProperty() {
return onSceneChanged;
}
public CustomStage() {
this(StageStyle.DECORATED);
}
public CustomStage(#NamedArg(value = "style", defaultValue = "DECORATED") StageStyle style) {
super(style);
sceneProperty().addListener((obs, ov, nv) -> fireEvent(new SceneChangedEvent(this, ov, nv)));
}
}
This would let you react to the scene changing using any of the following:
CustomStage stage = new CustomStage();
// addEventFilter/addEventHandler
stage.addEventFilter(SceneChangedEvent.SCENE_CHANGED, e -> { ... });
stage.addEventHandler(SceneChangedEvent.SCENE_CHANGED, e -> { ... });
// setOnSceneChanged
stage.setOnSceneChanged(e -> { ... });
Keep in mind that the event will only target the CustomStage instance. In other words, only event handlers added to the CustomStage instance will be notified of the event. And as you can see, this is much more complicated than simply adding a change listener to the scene property of the Stage.
Mouse events and scroll events behave in different ways
Mouse Events:
The event is captured by mainStage
The event is captured by mainStage
The event is not captured
Scroll Events:
The event is captured by mainStage
The event is captured by secondStage
The event is not captured
Is there any way that transparent secondStage does not capture scroll events?
My code:
Pane mainPane = new Pane(new Label("Main Stage"));
mainPane.setPrefSize(300, 300);
mainStage.setScene(new Scene(mainPane));
Stage secondStage = new Stage();
Pane secondPane = new Pane(new Label("Second Stage"));
secondPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
secondPane.setBorder(new Border(
new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(2))));
secondPane.setPrefSize(300, 300);
secondStage.setScene(new Scene(secondPane, Color.TRANSPARENT));
secondStage.initStyle(StageStyle.TRANSPARENT);
mainStage.getScene().setOnScroll(event -> System.out.println("Scroll in main stage"));
secondStage.getScene().setOnScroll(event -> System.out.println("Scroll in second stage"));
mainStage.getScene().setOnMouseClicked(event -> System.out.println("Click in main stage"));
secondStage.getScene().setOnMouseClicked(event -> System.out.println("Click in second stage"));
mainStage.show();
secondStage.show();
Java version: 1.8.0_201 (64 bits), Windows 10
edit:
The example is a simplification with only two windows. Fire the event programmatically implies discovering which stage is immediately lower and that is another problem in itself.
It might be a great coincidence, that we also came with the same solution of transparent window because of not having the feature of managing z-index of stages. And We encountered the exact same issue as yours. ie, scroll events not propagating to underlying Stages. We used the below approach, not sure whether this can help you:
Firstly, We constructed a Singleton class that keeps a reference of Node that is currently hovered on.
Then, when we create any normal stage, we include the below handlers to the scene of that new stage. The key thing here is that, the mouse events are still able to pass through the transparent stage to the underlying window, keep track of node which sits under the mouse.
scene.addEventFilter(MouseEvent.MOUSE_EXITED_TARGET, e -> {
hoverNode.set(null);
});
scene.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
hoverNode.set(e.getTarget());
});
In the scene of the transparent window, we included the below handlers to delegate the scroll events to the underlying node.
scene.addEventFilter(ScrollEvent.SCROLL, e -> {
if (hoverNode.get() != null) {
Event.fireEvent(hoverNode.get(), e);
}
});
scene.addEventHandler(ScrollEvent.SCROLL, e -> {
if (hoverNode.get() != null) {
Event.fireEvent(hoverNode.get(), e);
}
});
I am pretty sure this is not the most desired way. But this addressed our issue. :)
Below is the quick demo code of what I mean.
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.stream.IntStream;
public class ScrollThroughTransparentStage_Demo extends Application {
#Override
public void start(Stage stage) throws Exception {
stage.setTitle("Main Window");
VBox root = new VBox(buildScrollPane());
root.setStyle("-fx-background-color:#888888;");
root.setSpacing(10);
root.setPadding(new Insets(10));
Button normalStageBtn = new Button("Normal Stage");
normalStageBtn.setOnAction(e -> {
Stage normalStage = new Stage();
normalStage.initOwner(stage);
Scene normalScene = new Scene(buildScrollPane(), 300, 300);
addHandlers(normalScene);
normalStage.setScene(normalScene);
normalStage.show();
});
CheckBox allowScrollThrough = new CheckBox("Allow scroll through transparency");
allowScrollThrough.setSelected(true);
HBox buttons = new HBox(normalStageBtn);
buttons.setSpacing(20);
root.getChildren().addAll(allowScrollThrough,buttons);
Scene scene = new Scene(root, 600, 600);
addHandlers(scene);
stage.setScene(scene);
stage.show();
/* Transparent Stage */
Stage transparentStage = new Stage();
transparentStage.initOwner(stage);
transparentStage.initStyle(StageStyle.TRANSPARENT);
Pane mainRoot = new Pane();
Pane transparentRoot = new Pane(mainRoot);
transparentRoot.setStyle("-fx-background-color:transparent;");
Scene transparentScene = new Scene(transparentRoot, Color.TRANSPARENT);
transparentStage.setScene(transparentScene);
transparentScene.addEventFilter(ScrollEvent.SCROLL, e -> {
if (allowScrollThrough.isSelected() && HoverNodeSingleton.getInstance().getHoverNode() != null) {
Event.fireEvent(HoverNodeSingleton.getInstance().getHoverNode(), e);
}
});
transparentScene.addEventHandler(ScrollEvent.SCROLL, e -> {
if (allowScrollThrough.isSelected() && HoverNodeSingleton.getInstance().getHoverNode() != null) {
Event.fireEvent(HoverNodeSingleton.getInstance().getHoverNode(), e);
}
});
determineStageSize(transparentStage, mainRoot);
transparentStage.show();
Button transparentStageBtn = new Button("Transparent Stage");
transparentStageBtn.setOnAction(e -> {
MiniStage miniStage = new MiniStage(mainRoot);
ScrollPane scrollPane = buildScrollPane();
scrollPane.setPrefSize(300, 300);
miniStage.setContent(scrollPane);
miniStage.show();
});
buttons.getChildren().add(transparentStageBtn);
}
private static void determineStageSize(Stage stage, Node root) {
DoubleProperty width = new SimpleDoubleProperty();
DoubleProperty height = new SimpleDoubleProperty();
DoubleProperty shift = new SimpleDoubleProperty();
Screen.getScreens().forEach(screen -> {
Rectangle2D bounds = screen.getVisualBounds();
width.set(width.get() + bounds.getWidth());
if (bounds.getHeight() > height.get()) {
height.set(bounds.getHeight());
}
if (bounds.getMinX() < shift.get()) {
shift.set(bounds.getMinX());
}
});
stage.setX(shift.get());
stage.setY(0);
stage.setWidth(width.get());
stage.setHeight(height.get());
root.setTranslateX(-1 * shift.get());
}
private void addHandlers(Scene scene) {
scene.addEventFilter(MouseEvent.MOUSE_EXITED_TARGET, e -> {
HoverNodeSingleton.getInstance().setHoverNode(null);
});
scene.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
HoverNodeSingleton.getInstance().setHoverNode(e.getTarget());
});
}
private ScrollPane buildScrollPane() {
VBox vb = new VBox();
vb.setSpacing(10);
vb.setPadding(new Insets(15));
IntStream.rangeClosed(1, 100).forEach(i -> vb.getChildren().add(new Label(i + "")));
ScrollPane scrollPane = new ScrollPane(vb);
return scrollPane;
}
class MiniStage extends Group {
private Pane parent;
double sceneX, sceneY, layoutX, layoutY;
protected BorderPane windowPane;
private BorderPane windowTitleBar;
private Label labelTitle;
private Button buttonClose;
public MiniStage(Pane parent) {
this.parent = parent;
buildRootNode();
getChildren().add(windowPane);
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> toFront());
}
#Override
public void toFront() {
parent.getChildren().remove(this);
parent.getChildren().add(this);
}
public void setContent(Node content) {
// Computing the bounds of the content before rendering
Group grp = new Group(content);
new Scene(grp);
grp.applyCss();
grp.requestLayout();
double width = grp.getLayoutBounds().getWidth();
double height = grp.getLayoutBounds().getHeight() + 30; // 30 title bar height
grp.getChildren().clear();
windowPane.setCenter(content);
// Centering the stage
Rectangle2D screenBounds = Screen.getPrimary().getBounds();
setX(screenBounds.getWidth() / 2 - width / 2);
setY(screenBounds.getHeight() / 2 - height / 2);
}
public Node getContent() {
return windowPane.getCenter();
}
public void setX(double x) {
setLayoutX(x);
}
public void setY(double y) {
setLayoutY(y);
}
public void show() {
if (!parent.getChildren().contains(this)) {
parent.getChildren().add(this);
}
}
public void hide() {
parent.getChildren().remove(this);
}
private void buildRootNode() {
windowPane = new BorderPane();
windowPane.setStyle("-fx-border-width:2px;-fx-border-color:#444444;");
labelTitle = new Label("Mini Stage");
labelTitle.setStyle("-fx-font-weight:bold;");
labelTitle.setMaxHeight(Double.MAX_VALUE);
buttonClose = new Button("X");
buttonClose.setFocusTraversable(false);
buttonClose.setStyle("-fx-background-color:red;-fx-background-radius:0;-fx-background-insets:0;");
buttonClose.setOnMouseClicked(evt -> hide());
windowTitleBar = new BorderPane();
windowTitleBar.setStyle("-fx-border-width: 0 0 2px 0;-fx-border-color:#444444;-fx-background-color:#BBBBBB");
windowTitleBar.setLeft(labelTitle);
windowTitleBar.setRight(buttonClose);
windowTitleBar.setPadding(new Insets(0, 0, 0, 10));
windowTitleBar.getStyleClass().add("nonfocus-title-bar");
windowPane.setTop(windowTitleBar);
assignTitleBarEvents();
}
private void assignTitleBarEvents() {
windowTitleBar.setOnMousePressed(this::recordWindowLocation);
windowTitleBar.setOnMouseDragged(this::moveWindow);
windowTitleBar.setOnMouseReleased(this::resetMousePointer);
}
private final void recordWindowLocation(final MouseEvent event) {
sceneX = event.getSceneX();
sceneY = event.getSceneY();
layoutX = getLayoutX();
layoutY = getLayoutY();
getScene().setCursor(Cursor.MOVE);
}
private final void resetMousePointer(final MouseEvent event) {
// Updating the new layout positions
setLayoutX(layoutX + getTranslateX());
setLayoutY(layoutY + getTranslateY());
// Resetting the translate positions
setTranslateX(0);
setTranslateY(0);
getScene().setCursor(Cursor.DEFAULT);
}
private final void moveWindow(final MouseEvent event) {
double offsetX = event.getSceneX() - sceneX;
double offsetY = event.getSceneY() - sceneY;
setTranslateX(offsetX);
setTranslateY(offsetY);
event.consume();
}
}
}
/**
* Singleton class.
*/
class HoverNodeSingleton {
private static HoverNodeSingleton INSTANCE = new HoverNodeSingleton();
private EventTarget hoverNode;
private HoverNodeSingleton() {
}
public static HoverNodeSingleton getInstance() {
return INSTANCE;
}
public EventTarget getHoverNode() {
return hoverNode;
}
public void setHoverNode(EventTarget hoverNode) {
this.hoverNode = hoverNode;
}
}
I don't know that's right or not, but you can bind properties:
secondStage.getScene().onScrollProperty().bind(mainStage.getScene().onScrollProperty());
You can create a custom event dispatcher that will ignore events you don't want:
public class CustomEventDispatcher extends BasicEventDispatcher {
#Override
public Event dispatchEvent(Event event, EventDispatchChain tail) {
if(event instanceof ScrollEvent) {
return null;
} else {
return super.dispatchEvent(event, tail);
}
}
}
Then set that on your stage:
secondStage.setEventDispatcher(new CustomEventDispatcher());
I don't know how this works in the context of stages but for simple shapes it makes a difference whether you set the fill color to Color.TRANSPARENT or just null. Using any Color catches events, whereas null does not.
You can do so by ignoring the event on the second stage using event dispatcher using this answer by #Slaw you can understand everything about EventDispatcher
https://stackoverflow.com/a/51015783/5303683
Then you can fire your own event using this answer by DVarga
https://stackoverflow.com/a/40042513/5303683
Sorry I don't have time to try and make a full example of it
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'm loading a website on a JavaFX WebView and after a while taking a screenshot with something like:
WritableImage image = webView.snapshot(null, null);
If I'm looking at that WebView that works fine, but if it's hidden by being in a tab that is not in the foreground (I'm not sure about other cases of hiding it), then, the screenshot is of the appropriate site, but entirely blank.
How can I force the WebView to render even if not visible?
During this time, webView.isVisible() is true.
I found there's a method in WebView called isTreeReallyVisible() and currently it contains:
private boolean isTreeReallyVisible() {
if (getScene() == null) {
return false;
}
final Window window = getScene().getWindow();
if (window == null) {
return false;
}
boolean iconified = (window instanceof Stage) ? ((Stage)window).isIconified() : false;
return impl_isTreeVisible()
&& window.isShowing()
&& window.getWidth() > 0
&& window.getHeight() > 0
&& !iconified;
}
When the WebView is hidden by being in a non-foreground tab, impl_isTreeVisible() is false (all other factors in the return statement are true). That method is on Node and looks like this:
/**
* #treatAsPrivate implementation detail
* #deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
#Deprecated
public final boolean impl_isTreeVisible() {
return impl_treeVisibleProperty().get();
}
/**
* #treatAsPrivate implementation detail
* #deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
#Deprecated
protected final BooleanExpression impl_treeVisibleProperty() {
if (treeVisibleRO == null) {
treeVisibleRO = new TreeVisiblePropertyReadOnly();
}
return treeVisibleRO;
}
I could have overriden impl_treeVisibleProperty() to provide my own implementation, but WebView is final, so, I cannot inherit from it.
Another completely different situation to being minimized (iconified) or on a hidden tab is to have the stage completely hidden (as in, running in the tray bar). When in that mode, even if I can get rendering to happen, the WebView doesn't resize. I call webView.resize() and then take a screenshot and the screenshot is of the appropriate size but the actual rendered page is of whatever size the WebView was before.
Debugging this sizing behavior in shown and hidden stages, I found that eventually we get to Node.addToSceneDirtyList() that contains:
private void addToSceneDirtyList() {
Scene s = getScene();
if (s != null) {
s.addToDirtyList(this);
if (getSubScene() != null) {
getSubScene().setDirty(this);
}
}
}
When in hidden mode, getScene() returns null, unlike what happens when it's being show. That means that s.addToDirtyList(this) is never called. I'm not sure if this is the reason why it doesn't get properly resized.
There's a bug about this, a very old one, here: https://bugs.openjdk.java.net/browse/JDK-8087569 but I don't think that's the whole issue.
I'm doing this with Java 1.8.0_151. I tried 9.0.1 to see if it would behave differently as it is my understanding that WebKit was upgraded, but no, it's the same.
Reproducing Pablo's problem here: https://github.com/johanwitters/stackoverflow-javafx-webview.
Pablo suggested to override WebView and adjust some methods. That doesn't work given it's a final class and a private member. As an alternative, I've used javassist to rename a method and replace the code with the code that I want it to execute. I've "replaced" the contents of method handleStagePulse, as shown below.
public class WebViewChanges {
// public static String MY_WEBVIEW_CLASSNAME = WebView.class.getName();
public WebView newWebView() {
createSubclass();
return new WebView();
}
// https://www.ibm.com/developerworks/library/j-dyn0916/index.html
boolean created = false;
private void createSubclass() {
if (created) return;
created = true;
try
{
String methodName = "handleStagePulse";
// get the super class
CtClass webViewClass = ClassPool.getDefault().get("javafx.scene.web.WebView");
// get the method you want to override
CtMethod handleStagePulseMethod = webViewClass.getDeclaredMethod(methodName);
// Rename the previous handleStagePulse method
String newName = methodName+"Old";
handleStagePulseMethod.setName(newName);
// mnew.setBody(body.toString());
CtMethod newMethod = CtNewMethod.copy(handleStagePulseMethod, methodName, webViewClass, null);
String body = "{" +
" " + Scene.class.getName() + ".impl_setAllowPGAccess(true);\n" +
" " + "final " + NGWebView.class.getName() + " peer = impl_getPeer();\n" +
" " + "peer.update(); // creates new render queues\n" +
// " " + "if (page.isRepaintPending()) {\n" +
" " + " impl_markDirty(" + DirtyBits.class.getName() + ".WEBVIEW_VIEW);\n" +
// " " + "}\n" +
" " + Scene.class.getName() + ".impl_setAllowPGAccess(false);\n" +
"}\n";
System.out.println(body);
newMethod.setBody(body);
webViewClass.addMethod(newMethod);
CtMethod isTreeReallyVisibleMethod = webViewClass.getDeclaredMethod("isTreeReallyVisible");
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
This snippet is called from the WebViewSample which opens 2 tabs. One with a "snapshot" button, another with the WebView. As Pablo pointed out, the tab with the WebView needs to be the second tab to be able to reproduce.
package com.johanw.stackoverflow;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
public class WebViewSample extends Application {
private Scene scene;
private TheBrowser theBrowser;
private void setLabel(Label label) {
label.setText("" + theBrowser.browser.isVisible());
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Tabs");
Group root = new Group();
Scene scene = new Scene(root, 400, 250, Color.WHITE);
TabPane tabPane = new TabPane();
BorderPane borderPane = new BorderPane();
theBrowser = new TheBrowser();
{
Tab tab = new Tab();
tab.setText("Other tab");
HBox hbox0 = new HBox();
{
Button button = new Button("Screenshot");
button.addEventHandler(MouseEvent.MOUSE_PRESSED,
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
WritableImage image = theBrowser.getBrowser().snapshot(null, null);
File file = new File("test.png");
RenderedImage renderedImage = SwingFXUtils.fromFXImage(image, null);
try {
ImageIO.write(
renderedImage,
"png",
file);
} catch (IOException e1) {
e1.printStackTrace();
}
}
});
hbox0.getChildren().add(button);
hbox0.setAlignment(Pos.CENTER);
}
HBox hbox1 = new HBox();
Label visibleLabel = new Label("");
{
hbox1.getChildren().add(new Label("webView.isVisible() = "));
hbox1.getChildren().add(visibleLabel);
hbox1.setAlignment(Pos.CENTER);
setLabel(visibleLabel);
}
HBox hbox2 = new HBox();
{
Button button = new Button("Refresh");
button.addEventHandler(MouseEvent.MOUSE_PRESSED,
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
setLabel(visibleLabel);
}
});
hbox2.getChildren().add(button);
hbox2.setAlignment(Pos.CENTER);
}
VBox vbox = new VBox();
vbox.getChildren().addAll(hbox0);
vbox.getChildren().addAll(hbox1);
vbox.getChildren().addAll(hbox2);
tab.setContent(vbox);
tabPane.getTabs().add(tab);
}
{
Tab tab = new Tab();
tab.setText("Browser tab");
HBox hbox = new HBox();
hbox.getChildren().add(theBrowser);
hbox.setAlignment(Pos.CENTER);
tab.setContent(hbox);
tabPane.getTabs().add(tab);
}
// bind to take available space
borderPane.prefHeightProperty().bind(scene.heightProperty());
borderPane.prefWidthProperty().bind(scene.widthProperty());
borderPane.setCenter(tabPane);
root.getChildren().add(borderPane);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args){
launch(args);
}
}
class TheBrowser extends Region {
final WebView browser;
final WebEngine webEngine;
public TheBrowser() {
browser = new WebViewChanges().newWebView();
webEngine = browser.getEngine();
getStyleClass().add("browser");
webEngine.load("http://www.google.com");
getChildren().add(browser);
}
private Node createSpacer() {
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
return spacer;
}
#Override protected void layoutChildren() {
double w = getWidth();
double h = getHeight();
layoutInArea(browser,0,0,w,h,0, HPos.CENTER, VPos.CENTER);
}
#Override protected double computePrefWidth(double height) {
return 750;
}
#Override protected double computePrefHeight(double width) {
return 500;
}
public WebView getBrowser() {
return browser;
}
public WebEngine getWebEngine() {
return webEngine;
}
}
I've not succeeded in fixing Pablo's problem, but hopefully the suggestion to use javassist might help.
I'm sure: To be continued...
I would consider reloading page at the suitable moment using this command:
webView.getEngine().reload();
Also try to change parameter SnapshotParameters in method snapShot
If it would not work then I would consider storing Image in memory when WebView is being rendered on screen.
If you go by logical implementation of snapshot, only things that are visible on screen are taken as a snapshot.
For taking snapshot of the web view, you can either make it automatically visible by clicking on the tab inside which the view is rendered just before taking the snapshot. Or you can manually click on the tab and take the screenshot.
I think no API allows to take snapshot of hidden part as logically it will voilate the concept of hidden things.
Code for taking snapshot is already available with you. You can click, or Load the tab like:
You can add a selectionChangedListener or you can do the load just before the snapshot.
addItemTab.setOnSelectionChanged(event -> loadTabBasedFXML(addItemTab, "/view/AddItem.fxml"));
private void loadTabBasedFXML(Tab tab, String fxmlPath) {
try {
AnchorPane anchorPane = FXMLLoader.load(this.getClass().getResource(fxmlPath));
tab.setContent(anchorPane);
} catch (IOException e) {
}
}
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);
[...]