JavaFx: Scroll reset after TitledPane is collapsed - java

I am using TitledPanes ScrollPanes and TableViews and I have the problem, when I collapse a titledPane, the horizontal ScrollBar of the TableView resets.
Here is a code example where you can verify it:
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableView;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.AnchorPane;
import java.net.URL;
import java.util.ResourceBundle;
public class Controller implements Initializable {
#FXML
private AnchorPane content;
#FXML
private TitledPane titledPane;
#FXML
private TableView<Object> tableView;
#Override
public void initialize(URL location, ResourceBundle resources) {
titledPane.prefHeightProperty().bind(content.heightProperty());
tableView.prefWidthProperty().bind(content.widthProperty());
tableView.getColumns().forEach(col -> col.setPrefWidth(300)); // to have enough "space" to scroll
tableView.setItems(FXCollections.observableArrayList(new Object()));
}
}
FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="stackoverflow.testscroll.Controller"
fx:id="content">
<TitledPane fx:id="titledPane">
<TableView fx:id="tableView">
<columns>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
</columns>
</TableView>
</TitledPane>
</AnchorPane>
Any idea how can I prevent the scroll of the tableview to reset every time I collapse the pane?

After a bit of digging, it looks like some layout optimization in VirtualFlow might be the reason (all seems to be fine if the scrolled content is not a TableView - not thoroughly analyzed, though)
What happens is:
during collapse, the TitledPane's content is resized vertically to 0
in VirtualFlow's layoutChildren a zero height/width is special cased to do nothing except hide everything, including the scrollBars
an internal listener to the scrollBar's visiblilty resets its value to 0
A tentative (read: dirty and might have unwanted side-effects, totally untested beyond this quick outline!) hack around is a custom TableViewSkin that tries to "remember" the last not-zero value and resets it on getting visible again.
An example:
public class TitledPaneTableScroll extends Application {
public static class TableViewScrollSkin<T> extends TableViewSkin<T> {
DoubleProperty hvalue = new SimpleDoubleProperty();
public TableViewScrollSkin(TableView<T> control) {
super(control);
installHBarTweak();
}
private void installHBarTweak() {
// Note: flow and bar could be legally retrieved via lookup
// protected api pre-fx9 and post-fx9
VirtualFlow<?> flow = getVirtualFlow();
// access scrollBar via reflection
// this is my personal reflective access utility method - use your own :)
ScrollBar bar = (ScrollBar) FXUtils
.invokeGetFieldValue(VirtualFlow.class, flow, "hbar");
bar.valueProperty().addListener((s, o, n) -> {
if (n.intValue() != 0) {
hvalue.set(n.doubleValue());
// debugging
// new RuntimeException("who is calling? \n").printStackTrace();
}
//LOG.info("hbar value: " + n + "visible? " + bar.isVisible());
});
bar.visibleProperty().addListener((s, o, n) -> {
if (n) {
bar.setValue(hvalue.get());
}
});
}
}
int counter;
private Parent createContent() {
TableView<Object> table = new TableView<>(FXCollections.observableArrayList(new Object()) ) {
#Override
protected Skin<?> createDefaultSkin() {
return new TableViewScrollSkin<>(this);
}
};
table.getColumns().addAll(Stream
.generate(TableColumn::new)
.limit(10)
.map(col -> {
col.setPrefWidth(50);
col.setText("" + counter++);
return col;
})
.collect(Collectors.toList()));
TitledPane titled = new TitledPane("title", table);
titled.setAnimated(true);
BorderPane content = new BorderPane(titled);
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 400, 400));
// stage.setTitle(FXUtils.version());
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TitledPaneTableScroll.class.getName());
}

Related

ScrollPane doesnt "know" when to expand

I'm not sure how to call this problem but you will definitely understand what I mean...
at least I hope you will.
To start with: I have my Pane with a ScrollPane in it. This Pane also contains two Buttons on which I can create rectangles in my ScrollPane.
I can drag those Rectangles around and they can be moved out of view. This far everything works as it should be except the ScrollPane. Usually you had the ScrollPane expand his Scroll so you can, if you let go of the Rectangle, scroll to that Rectangle and grab it again but nice and friendly JavaFX doesnt know how to expand the Scroll so I cant get to this Rectangle again.
Or I just miss some important part (which is the more likely thing to happen)
Some words to my Code: I don't think that its a problem in my existing code but more a problem that needs a workaround. The only things that I changed for the ScrollPane are that I set the ScrollBarPolicy to always and set the ScrollPane Pannable but this is consumed when I click on a Rectangle so I can move them and don't pan the ScrollPane.
What you see above is one Rectangle in its full size and one which is partly out of the ScrollPane boundaries but the ScrollPane doesn't expand the ScrollBar.
Doesn't matter how far I move that Rectangle away from those boundaries the Scrollbar won't change which is bad for this is the only reason I chose a ScrollPane and also, I think, the only (easiest) pane to achieve this "editor like" view.
Yea so thank you for at least reading this far and like always sorry for my broken English ^^. I hope you can give me a advice on how to fix this problem!
EDIT:
Here a minimal example:
Controller.java
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Rectangle;
public class Controller implements Initializable{
#FXML ScrollPane actionWin;
Rectangle r;
Double orgTranslateX, orgTranslateY, orgSceneX, orgSceneY, newTranslateX, newTranslateY, offsetY, offsetX;
int i = 50;
List<Rectangle> recs = new ArrayList<Rectangle>();
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
actionWin.setPannable(true);
actionWin.setHbarPolicy(ScrollBarPolicy.ALWAYS);
actionWin.setVbarPolicy(ScrollBarPolicy.ALWAYS);
}
#FXML
public void createEntity(){
Pane container = new Pane();
// Button 1
r = new Rectangle();
r.addEventHandler(MouseEvent.ANY, event -> {
if(event.getButton() != MouseButton.MIDDLE) event.consume();
});
r.setOnMousePressed(onMousePressedEventHandler);
r.setOnMouseDragged(onMouseDraggedEventHandler);
r.setX(i);
r.setY(i);
r.setWidth(50);
r.setHeight(20);
i+=30;
recs.add(r);
for(Rectangle rec : recs){
container.getChildren().add(rec);
}
actionWin.setContent(container);
try {
AnchorPane root = (AnchorPane) FXMLLoader.load(getClass().getResource("Main.fxml"));
} catch (IOException e) {
e.printStackTrace();
}
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent arg0) {
orgSceneX = arg0.getSceneX();
orgSceneY = arg0.getSceneY();
orgTranslateX = ((Rectangle) (arg0.getSource())).getTranslateX();
orgTranslateY = ((Rectangle) (arg0.getSource())).getTranslateY();
}};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent arg0) {
offsetX = arg0.getSceneX() - orgSceneX;
offsetY = arg0.getSceneY() - orgSceneY;
newTranslateX = orgTranslateX + offsetX;
newTranslateY = orgTranslateY + offsetY;
((Rectangle)(arg0.getSource())).setTranslateX(newTranslateX);
((Rectangle)(arg0.getSource())).setTranslateY(newTranslateY);
}};
}
Main.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.Spinner?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane fx:id="root" maxHeight="720.0" maxWidth="1280.0" minHeight="720.0" minWidth="1280.0" prefHeight="720.0" prefWidth="1280.0" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller">
<children>
<Button fx:id="createEntity_B" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" onAction="#createEntity" text="Create Entity" />
<ScrollPane fx:id="actionWin" layoutX="14.0" layoutY="39.0" prefHeight="672.0" prefWidth="996.0" />
</children>
</AnchorPane>
Main.java
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
public class Main extends Application {
public void start(Stage primaryStage) {
try {
AnchorPane root = (AnchorPane)
FXMLLoader.load(getClass().getResource("Main.fxml"));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setResizable(false);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Wrap these three classes in a package called "application" and you're good to go.
You're using translateX and translateY to determine the new position of the Rectangles.
Transforms like the translate properties are not taken into account when the parent determines it's size. The child is treated as if it was located at (layoutX, layoutY). You need to modify those properties, if you want the Pane to be resized properly.
Some other things in the createEntity seem odd though:
You recreate the Pane instead of creating it once in the fxml.
You load a fxml that never seems to be used.
Also you should prefer the primitive type double to the reference type Double unless you're not doing any arithmetic operations or comparisons.
<AnchorPane fx:id="root" maxHeight="720.0" maxWidth="1280.0" minHeight="720.0" minWidth="1280.0" prefHeight="720.0" prefWidth="1280.0" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller">
<children>
<Button fx:id="createEntity_B" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" onAction="#createEntity" text="Create Entity" />
<ScrollPane fx:id="actionWin" layoutX="14.0" layoutY="39.0" prefHeight="672.0" prefWidth="996.0">
<content>
<Pane fx:id="container"/>
</content>
</ScrollPane>
</children>
</AnchorPane>
#FXML
private Pane container;
#FXML
public void createEntity() {
class DraggedHandler implements EventHandler<MouseEvent> {
double offsetX;
double offsetY;
#Override
public void handle(MouseEvent event) {
Node source = (Node) event.getSource();
Point2D pt = source.localToParent(event.getX(), event.getY());
source.setLayoutX(pt.getX() + offsetX);
source.setLayoutY(pt.getY() + offsetY);
}
}
Rectangle rect = new Rectangle(i, i, 50, 20);
i += 30;
container.getChildren().add(rect);
rect.addEventHandler(MouseEvent.ANY, event -> {
if (event.getButton() != MouseButton.MIDDLE) {
event.consume();
}
});
DraggedHandler handler = new DraggedHandler();
rect.setOnMouseDragged(handler);
rect.setOnMousePressed(evt -> {
Node source = (Node) evt.getSource();
Point2D pt = source.localToParent(evt.getX(), evt.getY());
handler.offsetX = source.getLayoutX() - pt.getX();
handler.offsetY = source.getLayoutY() - pt.getY();
});
}

How can I add 3D surface to the AnchorPane

How Can I add 3D surface to the view.fxml, there is not "thing" on the Scene Builder panel like surface.
My scene builder hierarchy looks like :
And ss of app - like we can see got something on the left top corner, the surface should be on the middle.
I would like to add first just some samples of 3D surface :
My controller code :
package sample.packet3D;
import org.fxyz.cameras.CameraTransformer;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.layout.AnchorPane;
public class Window3DController {
#FXML
private AnchorPane anchorPane;
#FXML
private Group group;
private Window3DBuilder window3dBuilder;
private PerspectiveCamera perspectiveCamera;
#FXML
public void initialize() {
perspectiveCamera = new PerspectiveCamera(true);
window3dBuilder = new Window3DBuilder( group, perspectiveCamera );
window3dBuilder.createScene();
group.sceneProperty().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
group.getScene().setCamera(perspectiveCamera);
group.sceneProperty().removeListener(this);
}
});
}
}
Logic class :
package sample.packet3D;
import org.fxyz.cameras.CameraTransformer;
import org.fxyz.shapes.primitives.SurfacePlotMesh;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
public class Window3DBuilder {
private Group group;
private SurfacePlotMesh surface;
private PerspectiveCamera perspectiveCamera;
private CameraTransformer cameraTransformer;
private PointLight light;
public Window3DBuilder( Group group, PerspectiveCamera perspectiveCamera ) {
this.group = group;
this.perspectiveCamera = perspectiveCamera;
cameraTransformer = new CameraTransformer();
}
public void createScene() {
createSurface();
createLight();
group.getChildren().addAll(surface);
cameraTransformer.setTranslate(0, 0, 0);
cameraTransformer.getChildren().addAll(perspectiveCamera);
perspectiveCamera.setNearClip(0.1);
perspectiveCamera.setFarClip(100000.0);
perspectiveCamera.setTranslateX((group.getBoundsInLocal().getMaxX() + group.getBoundsInLocal().getMinX()) / 2d);
perspectiveCamera.setTranslateY((group.getBoundsInLocal().getMaxY() + group.getBoundsInLocal().getMinY()) / 2d);
double max = Math.max(group.getBoundsInLocal().getWidth(), group.getBoundsInLocal().getHeight());
perspectiveCamera.setTranslateZ(-2 * max);
}
public void createLight() {
light = new PointLight(Color.WHITE);
cameraTransformer.getChildren().add(light);
light.setTranslateX(perspectiveCamera.getTranslateX());
light.setTranslateY(perspectiveCamera.getTranslateY());
light.setTranslateZ(perspectiveCamera.getTranslateZ());
}
private void createSurface() {
surface = new SurfacePlotMesh(
p-> Math.sin(p.magnitude() + 1e-10) / (p.magnitude() + 1e-10),
20, 20, 100, 100, 4);
surface.setCullFace(CullFace.NONE);
surface.setTextureModeVertices3D(1530, p -> p.magnitude());
surface.getTransforms().addAll(new Rotate(200, Rotate.X_AXIS), new Rotate(-20, Rotate.Y_AXIS));
}
}
And view :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.canvas.*?>
<?import javafx.scene.*?>
<?import javafx.scene.shape.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.packet3D.Window3DController">
<children>
<Group fx:id="group">
<effect>
<Lighting>
<bumpInput>
<Shadow />
</bumpInput>
<light>
<Light.Distant />
</light>
</Lighting>
</effect>
</Group>
<PerspectiveCamera fx:id="perspectiveCamera" visible="false" />
</children>
</AnchorPane>
What Am I doing wrong ? Could someone help me ?
Also this is one of the window, to which I am entering by pressing the button.
#FXML
public void moveTo3DScene(ActionEvent event) throws IOException {
Stage stage3D = (Stage) ((Node) event.getSource()).getScene().getWindow();
Parent parent3D = FXMLLoader.load(getClass().getResource("packet3D/Window3DSceneView.fxml"));
stage3D.setTitle("Animation 3D");
stage3D.setScene(new Scene(parent3D, 1200, 800));
stage3D.show();
}
You have a problem with PerspectiveCamera. It has a boolean parameter called fixedEyeAtCameraZero that by default is false, and a very small surface is shown at the top left corner of your scene.
We need to set it to true, so:
If fixedEyeAtCameraZero is true, the eye position is fixed at (0, 0, 0) in the local coordinates of the camera
But unfortunately you can't set the parameter, there is no setFixedEyeAtCameraZero() method. The only way to change it is with the camera constructor.
This means that you have to remove the PerspectiveCamera from the FXML file, and add it by code on the controller
public class Window3DController {
#FXML
private AnchorPane anchorPane;
#FXML
private Group group;
private Window3DBuilder window3dBuilder;
private PerspectiveCamera perspectiveCamera;
#FXML
public void initialize() {
perspectiveCamera = new PerspectiveCamera(true);
window3dBuilder = new Window3DBuilder(group, perspectiveCamera);
window3dBuilder.createScene();
group.sceneProperty().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
group.getScene().setCamera(perspectiveCamera);
group.sceneProperty().removeListener(this);
}
});
}
}
One last step: you need to set some parameters to the camera, basically its z coordinate based on the size of the mesh:
public void createScene() {
createSurface();
group.getChildren().addAll(surface);
perspectiveCamera.setNearClip(0.1);
perspectiveCamera.setFarClip(100000.0);
perspectiveCamera.setTranslateX((group.getBoundsInLocal().getMaxX() + group.getBoundsInLocal().getMinX()) / 2d);
perspectiveCamera.setTranslateY((group.getBoundsInLocal().getMaxY() + group.getBoundsInLocal().getMinY()) / 2d);
double max = Math.max(group.getBoundsInLocal().getWidth(), group.getBoundsInLocal().getHeight());
perspectiveCamera.setTranslateZ(-2 * max);
}
This will show your surface, but not as you will expect: the effects you are applying are intended for 2D:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="Window3DController">
<children>
<Group fx:id="group" />
</children>
</AnchorPane>
Remove those effects, and add them by code using PointLight:
public class Window3DBuilder {
private final Group group;
private SurfacePlotMesh surface;
private final CameraTransformer cameraTransformer;
private final PerspectiveCamera perspectiveCamera;
private PointLight light;
public Window3DBuilder( Group group, PerspectiveCamera perspectiveCamera ) {
this.group = group;
this.perspectiveCamera = perspectiveCamera;
cameraTransformer = new CameraTransformer();
}
public void createScene() {
createSurface();
group.getChildren().addAll(surface, cameraTransformer);
cameraTransformer.setTranslate(0, 0, 0);
cameraTransformer.getChildren().addAll(perspectiveCamera);
perspectiveCamera.setNearClip(0.1);
perspectiveCamera.setFarClip(100000.0);
perspectiveCamera.setTranslateX((group.getBoundsInLocal().getMaxX() + group.getBoundsInLocal().getMinX()) / 2d);
perspectiveCamera.setTranslateY((group.getBoundsInLocal().getMaxY() + group.getBoundsInLocal().getMinY()) / 2d);
double max = Math.max(group.getBoundsInLocal().getWidth(), group.getBoundsInLocal().getHeight());
perspectiveCamera.setTranslateZ(-2 * max);
createLight();
}
public void createLight() {
light = new PointLight(Color.WHITE);
cameraTransformer.getChildren().add(light);
light.setTranslateX(perspectiveCamera.getTranslateX());
light.setTranslateY(perspectiveCamera.getTranslateY());
light.setTranslateZ(perspectiveCamera.getTranslateZ() / 10);
}
private void createSurface() {
surface = new SurfacePlotMesh(
p-> Math.sin(p.magnitude() + 1e-10) / (p.magnitude() + 1e-10),
20, 20, 100, 100, 4);
surface.setCullFace(CullFace.NONE);
surface.setTextureModeVertices3D(1530, p -> p.magnitude());
surface.getTransforms().addAll(new Rotate(200, Rotate.X_AXIS), new Rotate(-20, Rotate.Y_AXIS));
}
}
I wanted to keep the camera in the .fxml file. I created a custom control based on PerspectiveCamera.
Start with a very simple class...
import javafx.scene.PerspectiveCamera;
public class PerspectiveCamera3D extends PerspectiveCamera {
// force 3D
public PerspectiveCamera3D() {
super(true);
}
// toss the parameter, force 3D
public PerspectiveCamera3D(final boolean fixedEyeAtCameraZero) {
this();
}
}
Export to a .jar file.
Launch Scene Builder and open the .fxml file for where you want the camera to be.
Open the 'gear' menu on the Library header.
Then Import FXML/Jar. Import your newly created .jar file. A dialog will pop up with your control listed. Once confirmed, the control will appear in the custom menu. Your control is now ready for use, just like any other one.
The 'Fixed Eye...' check box will still be read-only, but it will be checked. All of the other properties can be set as desired. To set the fx:id just add the following into your controller code...
#FXML
public PerspectiveCamera3D cambot;
Here is a bit more detailed of an example ... https://rterp.wordpress.com/2014/07/28/adding-custom-javafx-component-to-scene-builder-2-0-part-2/
The only problem I've had doing it this way, is when launching Scene Builder by clicking on the .fxml file in Eclipse causes an exception (I believe it is a local problem on my computer due to the way I'm launching things and their working directories). It works fine if I open Scene Builder, and then open the .fxml file from the Scene Builder File menu.

JavaFX TitledPane lookup(.title) returns null

I'm new to Java FX and am creating an application for fun. I'm trying to add a TitledPane dynamically and am getting Null Pointer Exceptions when attempting to lookup the title of the TitledPane about 70% of the time. I tried to create a simple demo of my issue, but was unable to reproduce the issue outside of my application, but I could solve my issue. I'm hoping someone could help me understand why my solution works and maybe point me in the direction of a better solution. I'm using an FXML file with a Controller. I'm attempting to lookup the title inside of Platform.runLater() because I'm manually editing the layout and elements of the title. Inside of the Controller's initialize function, I do the following to get null pointer exceptions:
Platform.runLater(new Runnable() {
#Override
public void run() {
titledpane.lookup(".title"); // will return null about 70% of the time
}
});
// Do more stuff
However, if I wrap that call in a timer and execute it in 500 ms, it doesn't seem to ever return Null.
new java.util.Timer().schedule(new java.util.TimerTask() {
#Override
public void run() {
Platform.runLater(new Runnable() {
#Override
public void run() {
titledpane.lookup(".title"); // doesn't seem to return null
}
});
}
}, 500);
One forum mentioned that CSS had to be applied to the element prior to calling a lookup on the title, so I tried manually applying CSS to the title, but titledpane.lookup(".title") still returned null. Can anyone help me understand what is happening here? Thanks in advance!
I had the same issue. I resolved it by calling applyCss() and layout() on the pane that contains the TitledPane:
Node loadedPane = paneLoader.load();
bodyPane.setCenter(loadedPane);
bodyPane.applyCss();
bodyPane.layout();
You should read the article Creating a Custom Control with FXML.
Here's an example about how you can load a TitledPane dynamically. A TitledPane is added each time you click the "Add Task" button.
Task.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<fx:root collapsible="false" prefHeight="72.0" prefWidth="202.0" text="Task" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<content>
<Pane prefHeight="43.0" prefWidth="200.0">
<children>
</children>
</Pane>
</content>
</fx:root>
Task.java
import java.io.IOException;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.Region;
public class Task extends Region {
TitledPane titledPane;
public Task( String title) {
final FXMLLoader fxmlLoader = new FXMLLoader( getClass().getResource( "Task.fxml"));
titledPane = new TitledPane();
fxmlLoader.setRoot( titledPane);
fxmlLoader.setController( this);
try {
fxmlLoader.load();
} catch( IOException exception) {
throw new RuntimeException( exception);
}
titledPane.setText(title);
getChildren().add( titledPane);
}
}
Demo.java
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Demo extends Application {
#Override
public void start(Stage primaryStage) {
Group root = new Group();
Button addTaskButton = new Button( "Add Task");
addTaskButton.setOnAction(new EventHandler<ActionEvent>() {
double x=0;
double y=0;
int count=0;
#Override
public void handle(ActionEvent event) {
// calculate new position
x+=50;
y+=50;
// task counter
count++;
Task task = new Task( "Task " + count);
task.relocate(x, y);
root.getChildren().add( task);
}
});
root.getChildren().add( addTaskButton);
Scene scene = new Scene( root, 1024, 768);
primaryStage.setScene( scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Screenshot:
There are various solutions in order to create a custom title. Here's one. Note: You need to provide an icon in the proper path or adapt the path. Alternatively you can just disable the ImageView node and instead use the Rectangle for demonstration purposes. It's just another node that's displayed.
public class Task extends Region {
TitledPane titledPane;
public Task( String title) {
final FXMLLoader fxmlLoader = new FXMLLoader( getClass().getResource( "Task.fxml"));
titledPane = new TitledPane();
fxmlLoader.setRoot( titledPane);
fxmlLoader.setController( this);
try {
fxmlLoader.load();
} catch( IOException exception) {
throw new RuntimeException( exception);
}
// create custom title with text left and icon right
AnchorPane anchorpane = new AnchorPane();
double offsetRight = 20; // just an arbitrary value. usually the offset from the left of the title
anchorpane.prefWidthProperty().bind(titledPane.widthProperty().subtract( offsetRight));
// create text for title
Label label = new Label( title);
// create icon for title
Image image = new Image( getClass().getResource( "title.png").toExternalForm());
ImageView icon = new ImageView();
icon.setImage(image);
// Rectangle icon = new Rectangle(16, 16);
// set text and icon positions
AnchorPane.setLeftAnchor(label, 0.0);
AnchorPane.setRightAnchor(icon, 0.0);
// add text and icon to custom title
anchorpane.getChildren().addAll( label, icon);
// set custom title
titledPane.setGraphic( anchorpane);
// show only our custom title, don't show the text of titlePane
titledPane.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
getChildren().add( titledPane);
}
}

JavaFX loading a stylesheet

JavaFX has a method that is added to the controller:
public void initialize(URL url, ResourceBundle rb)
This seems to run before any of the controls are added to the scene, because when I add this to it:
#Override
public void initialize(URL url, ResourceBundle rb){
String treeItemCss = getClass().getResource("/media/css/TreeItem.css").getPath();
main.getScene().getStylesheets().add(treeItemCss);
}
The CSS:
.tree-cell{
-fx-indent: 100;
-fx-underline: true;
}
I get an error from this method: getStylesheets(). But if I move that to an OnAction and execute that action I get no errors.
So my question is, is there a method that runs after all the controls are added to the scene, or a good way to add css to items that are created from a user action, such as a button click?
The initialize() method runs at the end of the FXMLLoader's load() method. Since you don't get a reference to the root of the FXML until that completes, there's obviously no way you can add it to a scene until after then.
You can:
Add the css to the Scene in the application code. I.e. Somewhere you create an FXMLLoader, call load(), and add the result to the Scene. Just set the css file on the scene right there, or:
Add the css stylesheet to the root node instead of to the scene (assuming main is a Parent):
public void initialize() {
String treeItemCss = ... ;
main.getStylesheets().add(treeItemCss);
}
or:
Observe the Scene property and add the stylesheet when it changes to something not null:
public void initialize() {
String treeItemCss = ... ;
main.sceneProperty().addListener((obs, oldScene, newScene) -> {
if (newScene != null) {
newScene.getStylesheets().add(treeItemCss);
}
});
}
Update Here is a complete example to demonstrate the second option. Everything is in the "application" package:
Main.java:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = FXMLLoader.load(getClass().getResource("Main.fxml"));
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);
}
}
Main.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TreeView?>
<?import javafx.scene.control.TreeItem?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainController" fx:id="root">
<center>
<TreeView>
<root>
<TreeItem value="Root">
<children>
<TreeItem value="One"/>
<TreeItem value="Two"/>
<TreeItem value="Three"/>
</children>
</TreeItem>
</root>
</TreeView>
</center>
</BorderPane>
MainController.java:
package application;
import javafx.fxml.FXML;
import javafx.scene.layout.BorderPane;
public class MainController {
#FXML
private BorderPane root ;
public void initialize() {
root.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
}
}
application.css:
.tree-cell{
-fx-indent: 100;
-fx-underline: true;
}
Note that you can add the stylesheet directly in the FXML file with
<BorderPane xmlns:fx="..." fx:controller="..." stylesheets="#application.css">
and then omit it completely from the controller logic.

Javafx Tableview Keep selected row in current view

I am using javafx tableview with active sorting and insertion of a new row every millisecond...
I want this functionality:
If I have selected a row then it should remain visible (that is should not go up or down from current visible portion of my table) when new rows are inserted.
This may be the long away around it and it's kind of hackish, but it worked for me when I needed to do something similar.
The gist of the answer is that you need to get access to the VirtualFlow member of the TableViewSkin. That's not as simple as is sounds because the skin isn't loaded until the CSS is parsed. I added a Listener to the skinProperty of the TableView and was able to get the VirtualFlow that way.
tableView.skinProperty().addListener(new ChangeListener<Skin>()
{
#Override
public void changed(ObservableValue<? extends Skin> ov, Skin t, Skin t1)
{
if (t1 == null) { return; }
TableViewSkin tvs = (TableViewSkin)t1;
ObservableList<Node> kids = tvs.getChildrenUnmodifiable();
if (kids == null || kids.isEmpty()) { return; }
flow = (VirtualFlow)kids.get(1);
}
});
After you have the VirtualFlow, you can listen to the table's ObservableList for changes and check to make sure the selected item is still in the viewport.
countries.addListener(new ListChangeListener<Country>()
{
#Override
public void onChanged(ListChangeListener.Change<? extends Country> change)
{
while (change.next())
{
if (change.wasAdded())
{
if (flow == null) { return; }
int first = flow.getFirstVisibleCell().getIndex();
int last = flow.getLastVisibleCell().getIndex();
int selected = tableView.getSelectionModel().getSelectedIndex();
if (selected < first || selected > last)
{
flow.show(selected);
}
}
}
}
});
There will still be a bit of bookkeeping to manage. For instance, if you wanted to place focus back on the table. Also, it's worth noting that the VirtualFlow isn't strictly bound by the visible rectangle of the table, so an item may be considered visible even if it is just outside the viewport. You can study the VirtualFlow for more details.
Here's a SSCCE.
JavaFXApplication21.java:
package javafxapplication21;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class JavaFXApplication21 extends Application
{
#Override
public void start(Stage stage) throws Exception
{
Parent root = FXMLLoader.load(getClass().getResource("Sample.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args)
{
launch(args);
}
}
Sample.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="200.0" prefWidth="200.0" xmlns:fx="http://javafx.com/fxml" fx:controller="javafxapplication21.SampleController">
<children>
<ToolBar AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<items>
<Button fx:id="insertBtn" mnemonicParsing="false" text="Insert" />
</items>
</ToolBar>
<TableView fx:id="tableView" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="31.0">
<columns>
<TableColumn prefWidth="100.0" text="Country" fx:id="countryColumn" />
<TableColumn prefWidth="100.0" text="Capital" fx:id="capitalColumn" />
</columns>
</TableView>
</children>
</AnchorPane>
SampleController.java:
package javafxapplication21;
import com.sun.javafx.scene.control.skin.TableViewSkin;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import java.net.URL;
import java.util.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
public class SampleController implements Initializable
{
#FXML private Button insertBtn;
#FXML private TableView<Country> tableView;
#FXML private TableColumn<Country, String> countryColumn;
#FXML private TableColumn<Country, String> capitalColumn;
private VirtualFlow flow;
private ObservableList<Country> countries =
FXCollections.observableArrayList();
private List<Country> insertList = new ArrayList<>();
public SampleController()
{
countries.addAll(new Country("AG", "Buenos Aires"),
new Country("AU", "Vienna"),
new Country("BY", "Minsk"),
new Country("CO", "Bogota"),
new Country("EG", "Cairo"));
insertList.add(new Country("ZI", "Harare"));
insertList.add(new Country("UK", "London"));
insertList.add(new Country("TW", "Taipei"));
}
#Override
public void initialize(URL url, ResourceBundle rb)
{
countryColumn.setCellValueFactory(
new PropertyValueFactory<Country, String>("name"));
capitalColumn.setCellValueFactory(
new PropertyValueFactory<Country, String>("capital"));
tableView.setItems(countries);
tableView.skinProperty().addListener(new ChangeListener<Skin>()
{
#Override
public void changed(ObservableValue<? extends Skin> ov,
Skin t, Skin t1)
{
if (t1 == null) { return; }
TableViewSkin tvs = (TableViewSkin)t1;
ObservableList<Node> kids = tvs.getChildrenUnmodifiable();
if (kids == null || kids.isEmpty()) { return; }
flow = (VirtualFlow)kids.get(1);
}
});
insertBtn.setOnAction(new EventHandler<ActionEvent>()
{
#Override
public void handle(ActionEvent t)
{
if (!insertList.isEmpty())
{
countries.add(2, insertList.get(0));
insertList.remove(0);
}
}
});
countries.addListener(new ListChangeListener<Country>()
{
#Override
public void onChanged(ListChangeListener.Change<? extends Country> change)
{
while (change.next())
{
if (change.wasAdded())
{
if (flow == null) { return; }
int first = flow.getFirstVisibleCell().getIndex();
int last = flow.getLastVisibleCell().getIndex();
int selected = tableView.getSelectionModel().getSelectedIndex();
if (selected < first || selected > last)
{
flow.show(selected);
}
}
}
}
});
}
public class Country
{
private SimpleStringProperty name;
private SimpleStringProperty capital;
public Country(String name, String capital)
{
this.name = new SimpleStringProperty(name);
this.capital = new SimpleStringProperty(capital);
}
public SimpleStringProperty nameProperty() { return name; }
public SimpleStringProperty capitalProperty() { return capital; }
}
}
I also had the same problem .You can use cellEventHandler class to which implemets EventHandler interface to fired when a cell is selcted in table and set last selected row index to a variable. The class is as followed.
public class CellEventHandler implements EventHandler<MouseEvent>
{
CellEventHandler()
{
}
#Override
public void handle(MouseEvent t)
{
TableCell c;
c = (TableCell) t.getSource();
c.getIndex();
PanelController.selectedItem = c.getIndex();
}
}`
Then your table class render of the table in PanelController Class should be as followed.
typeCol.setCellFactory(new Callback<TableColumn<tableModel, String>, TableCell<tableModel, String>>()
{
#Override
public TableCell<tableModel, String> call(TableColumn<tableModel, String> tableColumn)
{
LableCell lableCell = new LableCell(Pos.CENTER, true);
lableCell.addEventFilter(MouseEvent.MOUSE_CLICKED, new CellEventHandler());
return lableCell;
}
});
The "selectedItem" field should be declared in the panelController class as static field and then that "selectedItem " should set as selected in the table when you refresh the table row data.
myTable.getSelectionModel().select(selectedItem);
This works with me:
tableView.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, t1) -> {
TableViewSkin tvs = (TableViewSkin)tableView.getSkin();
ObservableList<Node> kids = tvs.getChildren();
if (kids == null || kids.isEmpty()) { return; }
VirtualFlow flow = (VirtualFlow)kids.get(1);
int first =-1;
if (flow.getFirstVisibleCell() != null) first = flow.getFirstVisibleCell().getIndex();
int last = -1;
if (flow.getFirstVisibleCell() != null) last = flow.getLastVisibleCell().getIndex();
if(first !=-1 && t1.intValue()<first)
flow.scrollTo(t1.intValue());
if(last != -1 && t1.intValue()>last)
flow.scrollTo(t1.intValue()+1);
});

Categories