JavaFX TabPane tabs don't update position - java

I noticed that when adding and deleting tabs from a TabPane, it fails to match the position of the order of tabs in the underlying list. This only happens when at least one tab is hidden entirely due to the width of the parent. Here's some code that replicates the issue:
public class TabPaneTester extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Scene scene = sizeScene();
primaryStage.setMinHeight(200);
primaryStage.setWidth(475);
primaryStage.setScene(scene);
primaryStage.show();
}
private Scene sizeScene(){
TabPane tabPane = new TabPane();
tabPane.setTabMinWidth(200);
tabPane.getTabs().addAll(newTabs(3));
Scene scene = new Scene(tabPane);
scene.setOnKeyPressed(e -> tabPane.getTabs().add(1, tabPane.getTabs().remove(0)));
return scene;
}
private static Tab[] newTabs(int numTabs){
Tab[] tabs = new Tab[numTabs];
for(int i = 0; i < numTabs; i++) {
Label label = new Label("Tab Number " + (i + 1));
Tab tab = new Tab();
tab.setGraphic(label);
tabs[i] = tab;
}
return tabs;
}
public static void main(String[] args) {
launch();
}
}
When you press a key, it removes the first tab (at index 0) and puts it back at index 1, effectively swapping the first two tabs. However, when run the tabs don't actually visually swap (even though the tab switcher menu does switch their position).
If you change the width of the screen to include even a pixel of the third tab that was hidden (replace 475 with 500), it works as intended. Any clues as to how to fix this?

This is indeed a bug and I couldn't find it reported in the public JIRA it is now reported at https://bugs.openjdk.java.net/browse/JDK-8193495.
All my analysis is based on the code in TabPaneSkin if you want to have a look yourself.
Summary
The problem arises when you remove and then add the tab "too quickly". When a tab is removed, asynchronous calls are made during the removal process. If you make another change such as adding a tab before the async calls finish (or at least "finish enough"), then the change procedure sees the pane at an invalid state.
Details
Removing a tab calls removeTabs, which is outlined below:
Various internal removal methods are called.
Then it checks if closing should be animated.
If yes (GROW),
an animation queues a call to a requestLayout method, which itself is invoked asynchronously,
and the animations starts (asynchronously) and the method returns.
If not (NONE),
requestLayout is called immediately and the method returns.
The time during which the pane is at an invalid state is the time from when the call returns until requestLayout returns (on another thread). This duration is equivalent to the duration of requestLayout plus the duration of the animation (if there is one), which is ANIMATION_SPEED = 150[ms]. Invoking addTabs during this time can cause undesired effects because the data needed to properly add the tab is not ready yet.
Workaround
Add an artificial pause between the calls:
ObservableList<Tab> tabs = tabPane.getTabs();
PauseTransition p = new PauseTransition(Duration.millis(150 + 20));
scene.setOnKeyPressed(e -> {
Tab remove = tabs.remove(0);
p.setOnFinished(e2 -> tabs.add(1, remove));
p.play();
});
This is enough time for the asynchronous calls to return (don't call the KeyPressed handler too quickly in succession because you will remove the tabs faster than they can be added). You can turn off the removal animation with
tabPane.setStyle("-fx-close-tab-animation: NONE;");
which allows you to decrease the pause duration. On my machine 15 was safe (here you can also call the KeyPressed handler quickly in succession because of the short delay).
Possible fix
Some synchronization on tabHeaderArea.

Related

How to perform JavaFX scene graph updates without affecting animation performance

I'm looking for advice on updating the JavaFX scene graph while an animation is running. What I have is a custom level of detail node in a 3D scene that loads and creates a MeshView in a background thread. That's fine, it works without affecting the JavaFX application thread. But the issue is that when I update the node (I actually just replace a child in a Group node) that is live in the scene graph like this (running this part on the JavaFX application thread):
Group group = (some value that is a live node in the scene graph ...)
group.getChildren().set (0, response.view); <-- response.view is my MeshView instance
it works, but a large number of changes like this being pushed to the scene in a small span of time makes the animation of the 3D scene shudder. I know why it's happening -- the pulse is doing a lot of work to update the scene graph to the graphics card. But I'd like some advice and/or code samples on how best to handle this. One thing I can think of would be to use a producer/consumer model where the scene graph changes are produced and placed into a queue, and then the consumer would only consume so many at a time. But there are times when the animation stops and isn't running anymore, and the scene graph changes can be pushed to the scene as fast as they become available.
Is there an example of handling this well online somewhere that I haven't found? Or some standard practices / solutions that are useful? Basically it's like I want to ensure the frame rate of the animation and only push changes at a rate that can be handled without disrupting the animation rate, but I have no idea how to determine what rate that would be. I don't know how to measure the length of time that each of my scene graph modifications are actually taking behind the scenes, or the rate of pulses that are falling below the usual 60 Hz so that I can throttle back my impact if needed.
So I've got something that works well. Whenever I need to perform a scene graph update and I don't know if an animation is running or not (because I'm in another class), I wrap the scene graph update in a Runnable that's accepted by a Consumer:
private Consumer<Runnable> updateConsumer;
...
updateConsumer.accept (() -> group.getChildren().set (0, response.view));
Then in the class that is the final destination of the consumer, I add the scene graph update to queue:
private ConcurrentLinkedQueue<Runnable> sceneGraphChangeQueue;
...
public void addSceneGraphChange (Runnable change) {
sceneGraphChangeQueue.add (change);
}
When an animation is being performed, I watch a variable that I know is changing for each step of the animation, and attach a listener to it so that it can track the animation is happening. In my case, I'm tracking my camera position and I hold onto the count of camera position updates since I last reset the counter (which I'll explain the purpose for below):
private int cameraUpdatesSinceReset;
...
view.cameraPositionProperty().addListener ((obs, oldVal, newVal) -> {
cameraUpdatesSinceReset++;
performSceneChanges (SCENE_CHANGES_PER_PULSE);
});
...
private void performSceneChanges (
int limit
) {
int count = 0;
while (!sceneGraphChangeQueue.isEmpty() && count < limit) {
Runnable change = sceneGraphChangeQueue.remove();
change.run();
count++;
} // while
if (count != 0) LOGGER.finer ("Performed " + count + " scene graph changes");
} // performSceneChanges
So that lets the scene graph changes trickle through during each animation pulse, and I found that setting SCENE_CHANGES_PER_PULSE=2 was about all my machine can handle before the animation starts to shudder. Finally, to make sure the scene change queue is flushed when there are no animations going on anymore, I poll the queue periodically with a ScheduledService and check in on my cameraUpdatesSinceReset counter:
ScheduledService<Void> service = new ScheduledService<Void>() {
protected Task<Void> createTask() {
return new Task<Void>() {
protected Void call() {
Platform.runLater (() -> {
if (cameraUpdatesSinceReset == 0) {
performSceneChanges (Integer.MAX_VALUE);
} // if
else cameraUpdatesSinceReset = 0;
});
return (null);
} // call
};
} // createTask
};
service.setPeriod (Duration.millis (333));
service.start();
The service polls the queue three times per second, which is enough to have 20 pulses pass by and if the camera hasn't updated in that time, I assume there's no animation or user interaction going on. The result is that the animation is smooth and the system does as many scene graph updates as it can when it can.

Java application - swing - pop up on right click changes background colour

I am having issues with my java application and I can't find a suitable reply.
In summary a right click triggered default pop up menu changes my chart's background color behind the pop up.
You can find images below. I am happy to keep the default popup without the "buggy" behaviour or develop my own if required.
A click of a button starts a stream of data and adds a chart to a JInternalFrame component:
If I right click on the image a default pop up comes up:
If I then click away the rectangle area covered by the popup will overlay the chart like this:
TimeseriesMonitorModel model = new DefaultTweetMonitorModel();
jif.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
try {
jif.setContentPane(new TweetSeriesChartPane(model, TweetMonitor.keywords, tkc));
jif.setSize(jif.getWidth(), jif.getHeight());
} catch (InterruptedException ex) {
Logger.getLogger(TweetMonitor.class.getName()).log(Level.SEVERE, null, ex);
}
jif.setVisible(true);
where jif is the Jinternalframe and
public TweetSeriesChartPane(TimeseriesMonitorModel model, String[] seriesNames, TweetKeywordCount tkc) throws InterruptedException {
this.seriesNames = seriesNames;
this.tkc = tkc;
this.model = model;
XYChartTimeseries myRealTimeChart = new XYChartTimeseries();
chart = myRealTimeChart.getChartWithTitle();
List[] tweetData = model.getFrequencyCount(new AtomicIntegerArray(seriesNames.length)); // we are starting from 0
int i = 0;
for (String keyword : seriesNames) {
List<Integer> yData = (List<Integer>) tweetData[1].get(i);
chart.addSeries(keyword, tweetData[0], yData); // adding first value
i++;
}
setLayout(new BorderLayout());
XChartPanel<XYChart> chartPane = new XChartPanel<>(chart);
add(chartPane);
UpdateWorker worker = new UpdateWorker(this, seriesNames, this.tkc);
worker.execute();
}
I've managed to temporary solve the above.
I've checked specifically this line of code
XChartPanel<XYChart> chartPane = new XChartPanel<>(chart);
which extends Chart and Jpanel. The code is from the knowm-chart dependency https://github.com/knowm/XChart/blob/develop/xchart/src/main/java/org/knowm/xchart/XChartPanel.java)
Apparently it adds a listener and the customized PopUpMenu. After reviewing it, it looks like it didn't do any repainting when the mouse was clicked outside the PopUpMenu area.
So I created a new class and tried to customise it. However, the repaint was flickering the screen and I couldn't get it to work only in the PopUpMenu area.
I ended up disabling the .addMouseListener call so now I don't get any popUpMenu. I sad compromise, but oh well.
BTW:
Thanks to both, regardless of the last unneeded comment which didn't add any value.
I did read the link and I though I provided enough information.
In any case, posting to code helped me troubleshoot it

ListView with custom CellFactory trims invisible nodes

My layout issue
I have a little issue with ListView and I'm not sure if it's because of some knowledge I missing or if my approach is flawed. Have to admit I'm not yet clear with how JavaFX handle the layout in the many possible cases.
The above screenshot shows the result I get twice with the exact same code, except that on the second one an invisible shape I use for coherent layout is made visible for debug.
The various classes involved by the CellFactory extend Group, I tried with some other Parent without much success so far.
How to reproduce
Rather than sharing my StarShape, StarRow and some other misc classes (I'd be happy to if requested) I wrote a sample reproducing the issue. The class extends Application and overrides the start(...) method as such:
#Override
public void start(Stage primaryStage) throws Exception {
final StackPane root = new StackPane();
final Scene scene = new Scene(root, 400, 600);
final ListView<Boolean> listView = new ListView<>();
listView.setCellFactory(this::cellFactory);
for (int i = 0; i < 5 ; i++) {
listView.getItems().add(true);
listView.getItems().add(false);
}
root.getChildren().add(listView);
primaryStage.setScene(scene);
primaryStage.setTitle("ListView trims the invisible");
primaryStage.show();
}
where this::cellFactory is
private ListCell<Boolean> cellFactory(ListView<Boolean> listView) {
return new ListCell<Boolean>() {
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
final Rectangle tabShape = new Rectangle();
tabShape.setHeight(20);
tabShape.setWidth(40);
tabShape.setVisible(item);
final Label label = new Label(item.toString());
label.setLayoutX(40);
final Group cellRoot = new Group();
cellRoot.getChildren().add(tabShape);
cellRoot.getChildren().add(label);
setGraphic(cellRoot);
}
}
};
}
The above will display a ListView<Boolean> with black shapes in front of true items (because of the tabShape.setVisible(item); bit). The false items are looking like regular Label objects as if the invisible shape in their Group wasn't there (but it is).
Closing comments
Debugging this, it turns out groups with the invisible shapes are given negative layoutX property values. Thus Label controls aren't aligned as I'd like them to be. It doesn't happen when I call setLayoutX and setLayoutY outside of a ListView (the invisible shapes do force offsets), but it's probably not the only place where it would happen.
What's happening and how to avoid it? Alternatively, as I'm guessing I'm approaching this wrong, what'd be the right way? In other words, what is the question I should be asking instead of this?
Taking from #dlatikay's comment, instead of setting the placeholder items to invisible, you can render them transparent by setting their opacity to 0.0.
Applied to the MCVE from your question, this would be done by replacing:
tabShape.setVisible(item);
with:
tabShape.setOpacity(item ? 1.0 : 0.0);
In terms of user experience, you could take this one step further. Instead of setting the "inactive" stars to fully transparent, you could set them to be near-transparent, as in this mockup (with opacity set to 0.1):
The benefits that I see are:
It indicates not only the rating of an item in the list, but also the maximum rating.
It avoids awkward empty spaces for list items with zero stars.
I'm guessing I'm approaching this wrong
No, you're not. As with all layouts, there's often multiple ways to approach the same problem. Your approach is actually correct, and you're very close to a working solution.
You can achieve what you're after with a mere 1 line change. That is, changing the Group to an HBox.
An HBox ensures that elements are ordered horizontally, one after another. They also allow invisible elements to still take up space.
I also commented out one line: label.setLayoutX(40). I did this because HBox will not respect this setting, and actually you don't need it to. It will automatically shift the elements horizontally by as much is required.
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
}
else {
final Rectangle tabShape = new Rectangle();
tabShape.setHeight(20);
tabShape.setWidth(40);
tabShape.setVisible(item);
final Label label = new Label(item.toString());
//label.setLayoutX(40);
final HBox cellRoot = new HBox();
cellRoot.getChildren().add(tabShape);
cellRoot.getChildren().add(label);
setGraphic(cellRoot);
}
}
When I make those changes, your layout will render like so:
Important: Your example and your screenshots are slightly different. You may want to use a VBox for your star example (V for 'vertical', H for 'horizontal').

How to clone a node in the scene graph in JavaFX?

I have a HBox with prefHeight = 70 // no prefWidth or any width...
I also have a Pane with prefWidth = 50 // no prefHeight or any height...
I just want to add multiple instance of the pane to the HBox using some loop.
When I do add(pane) in the loop body it gives following error.
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = HBox[id=myHBox]
I need to find way to clone the pane(as it passes by value).
Can anybody help me please?
(taking snapshot are not work for me because prefHeight of the pane is not set/ computed using parent)
This error happens because you're trying to add the same instance of a Node to another Node. If you remove the comments from the example below you'll get that error as well. Loop, on the other hand, will work fine because in each iteration new Button instance is created.
#Override
public void start(Stage stage) {
FlowPane root = new FlowPane();
// Results in error
// Button b1 = new Button("Button");
// root.getChildren().addAll(b1,b1);
for (int i = 0; i < 4; i++) {
Button b = new Button("Button");
root.getChildren().add(b);
}
Scene scene = new Scene(root, 50, 100);
stage.setScene(scene);
stage.show();
}
Your pane is probably more complicated, but you have to use the same principle. Put the code responsible for creating your pane in a separate method, getPane() or such, and use it in a loop to obtain new instances.
JavaFX doesn't give you an out-of-the-box solution to make a deep copy of a Node. If your Node is crated statically you can:
Put the code responsible for creating it in a separate method and
use it throughtout your application every time you need to get a new
instance of your pane.
Define it in a FXML file and load it every time you need a new instance.
Things get significantly worse if your Node has properties or children that were created or modified dynamically by the user. In that case you have to inspect its elements and recreate them on your own.

Regulating the number of executions per second using JavaFX

So as of right now I'm implementing Conway's Game of Life using JavaFX. In a nutshell, in my class extending AnimationTimer, within the handle() method, it traverses through every cell in a 2D array and updates each position, then draws to the canvas using the information in the 2D array.
This works completely fine but the problem is it runs far too fast. You can't really see what's going on on the canvas. On the window I have the canvas as well as a few buttons. I added a Thread.sleep(1000) to try to regulate a generation/frame per second, but doing this causes the window to not detect the button presses immediately. The button presses are completely responsive when not telling the thread to sleep.
Does anyone have any suggestions on how to solve this?
You can use Timeline which is probably more suitable for this. Set cycle count to Animation.INDEFINITE, and add a KeyFrame with the delay you want between updates, and your current handle implementation as the frame's onFinished.
final Timeline timeline = new Timeline();
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.getKeyFrames().add(
new KeyFrame(
Duration.seconds(1),
event -> handle()
)
);
timeline.play();
Alternatively, you may try to have the delay of the KeyFrame as zero, and use the Timeline's targetFrameRate, but I personally never tried it.
Edit: Another option is to keep a frameSkip variable in your AnimationTimer:
private int frameSkip = 0;
private final int SKIP = 10;
#Override
public void handle(long now) {
frameSkip++;
if (frameSkip <= SKIP) {
// Do nothing, wait for next frame;
return;
}
// Every SKIP frames, reset frameSkip and do animation
frameSkip = 0;
// Do animation...
}

Categories

Resources