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').
Related
I'm working on an Java app that changes components size after it's window size changes and i have a problem with ComboBox dropdown. After window size changes, first expanding of the dropdown doesnt change it's width and i get this. When i expand the list second time, it works well, but only when i use following code:
comboBoxWindowSize.setCellFactory(new Callback<ListView<WindowSize>, ListCell<WindowSize>>() {
#Override
public ListCell<WindowSize> call(ListView<WindowSize> param) {
ListCell cell = new ListCell<WindowSize>() {
#Override
public void updateItem(WindowSize windowSize, boolean empty) {
super.updateItem(windowSize, empty);
setPrefHeight(padH25);
getListView().setPrefWidth(padW150);
if (!empty) {
setText(windowSize.toString());
} else {
setText(null);
}
}
};
return cell;
}
});
When i don't use this code, width stays incorrect all the time. I want dropdown width to be equal to ComboBox width. Will you help me fix that?
Ok, i found an answer. It's very ugly, but it actually works.
Just like i said above, it's size is incorrect only in the first expanding. So all i had to do, is to expand and collapse it programmatically after i change window size:
comboBoxWindowSize.show();
comboBoxWindowSize.hide();
And thats all.
I have a single column TableView display Labels with an icon. What I'd like to do is add an animated icon to the label or cell so users know it's loading. I'm not sure how to go about this. I've looked around the internet and haven't come up with anything. I haven't had any luck trying to extend the label and customize the drawing, it's not quite as straight forward as overriding a paint method. So if anyone could point me in the right direction on how I could achieve this effect.
I think you cannot do that since the cell renderer will travel across cells and invoke paint for each cell. So I don't think you can do that and show animated icons. What you can do is to change some property of the row that is loading and invoke transition.
You can get some ideas from this code and get some similar effect:
https://github.com/james-d/Animated-Table-Row/blob/master/src/animatedtablerow/AnimatedTableRow.java
Thought I already posted this but in case this comes up for anyone else looking to do something similar, here was my solution:
sourceColumn.setCellFactory(new Callback<TableColumn<SourceItem, SourceItem>,TableCell<SourceItem, SourceItem>>(){
public TableCell<SourceItem, SourceItem> call(TableColumn<SourceItem, SourceItem> param){
TableCell<SourceItem, SourceItem> cell = new TableCell<SourceItem, SourceItem>(){
#Override
public void updateItem(SourceItem item, boolean empty) {
if (!empty) {
HBox box = new HBox();
box.setAlignment(Pos.CENTER_LEFT);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
box.getChildren().addAll(item.getLabel(), spacer);
if (item instanceof ShareSourceItem) {
if (((ShareSourceItem)item).isResolving()) {
box.getChildren().addAll(loadImage(mediaPath+"loader2.gif"));
}
}
setGraphic(box);
}
}
};
return cell;
}
});
I am making a grid-style game/simulation based on bugs "sensing" and eating food. I am using a gridPane (called worldGrid) of labels to show the grid of bugs and food. This is obviously going to be constantly updated when a bug moves cells towards food etc.
I currently have a function updateGrid(int col, int row, String cellContent) which I want to replace the label at [row,col] with a label that has the new text in cellContent.
I have the follow which works
worldGrid.add(new Label(cellContent), row,col);
however im worried that that is just adding a label on top of the current label and obviously over 100 iterations of the simulation thats not ideal.
I have tried this before adding the label:
worldGrid.getChildren().remove(row,col);
However I then get an IllegalArgumentException when trying to do the add line.
Any ideas on how to do this? Or even better, any ideas on how best to show a constantly changing grid that will eventually use sprites instead of text?
The col/row provided by grid.add(node, col, row) (ATTENTION first comes col!) is only a layout constraint. This does not provide any means to access columns or rows like slots in a 2-dimensional array. So to replace a node, you have to know its object itself, e.g. remember them in a separate array.
Then you are able to call getChildren().remove(object)... e.g.:
GridPane grid = new GridPane();
Label first = new Label("first");
Label second = new Label("second");
grid.add(first, 1, 1);
grid.add(second, 2, 2);
second.setOnMouseClicked(e -> {
grid.getChildren().remove(second);
grid.add(new Label("last"), 2, 2);
});
box.getChildren().addAll(grid);
I agree with Jens-Peter but I would add that you can use GridPane's getColumnIndex and getRowIndex to obtain a particular node's location.
For example ( this is in a component which extends GridPane ):
// set the appropriate label
for (Node node : getChildren()) {
if (node instanceof Label
&& getColumnIndex(node) == column
&& getRowIndex(node) == row) {
((Label)node).setTooltip(new Tooltip(tooltip));
}
}
in other words, go through all the nodes in the GridPane and check for a match of the conditions you want, in this case row and column.
You called getChildren().remove() method directly will cause the gridpane to go out of sync with the constraints. When you add, it also setup the constraint for the node. Add clearConstraints() method.
So, my issue is this: I'm attempting to define a custom set of nodes for a Javafx XYChart LineChart, where each node corresponds to a point that was plotted directly from the datasets. After looking around a little bit, Jewlesea actually had a solution at one point about how to add dynamic labels to nodes on a linechart graph that gave me enough of a push in the right direction to create black symbols (they are dots at the moment, but they can be many different things). Now I have a requirement that requires me to change ONE of the nodes on the XY chart into an 'X'. this could be either through loading an image in place of the 'node', or through physically manipulating the 'shape' parameter in .css.
The problem begins when I try to add this property dynamically, since which node has the 'x' will always be changing. Here are the things I've tried, and they all end up with no results whatsoever, regardless of the property used.
private XYChart.Data datum( Double x, Double y )
{
final XYChart.Data data = new XYChart.Data(x, y);
data.setNode(
new HoveredThresholdNode(x, y));
//data.getNode().setStyle("-fx-background-image: url(\"redX.png\");");
data.getNode().styleProperty().bind(
new SimpleStringProperty("-fx-background-color: #0181e2;")
.concat("-fx-font-size: 20px;")
.concat("-fx-background-radius: 0;")
.concat("-fx-background-insets: 0;")
.concat("-fx-shape: \"M2,0 L5,4 L8,0 L10,0 L10,2 L6,5 L10,8 L10,10 L8,10 L5,6 L2,10 L0,10 L0,8 L4,5 L0,2 L0,0 Z\";")
);
data.getNode().toFront();
return data;
}
So in the above, you can see that this is adding a property through the use of the 'bind' function after the dataNode has already been created. Also note above, I tried doing it through the 'setStyle' interface at this level to give it a background image, with no success. Also, no errors are being thrown, no 'invalid css' or anything of the sort, just simply no display on the graph at all when done this way.
now, in the HoveredThresholdNode (Again a big thanks to Jewelsea for being a master of Javafx and putting this bit of code online, it's where 90% of this class came from.) I tried essentially the same thing, at a different level. (actually being IN the node creation class, as opposed to a layer above it).
class HoveredThresholdNode extends StackPane {
/**
*
* #param x the x value of our node (this gets passed around a bunch)
* #param y the y value of our node (also gets passed around a bunch)
*/
HoveredThresholdNode(Double x, Double y) {
//The preferred size of each node of the graph
//getStylesheets().add(getClass().getResource("style/XYChart.css").toExternalForm());
//getStyleClass().add("xyChart-Node");
//setOpacity(.8);
styleProperty().bind(
new SimpleStringProperty("-fx-background-color: #0181e2;")
.concat("-fx-font-size: 20px;")
.concat("-fx-background-radius: 0;")
.concat("-fx-background-insets: 0;")
.concat("-fx-shape: \"M2,0 L5,4 L8,0 L10,0 L10,2 L6,5 L10,8 L10,10 L8,10 L5,6 L2,10 L0,10 L0,8 L4,5 L0,2 L0,0 Z\";")
);
//this label is the 'tooltip' label for the graph.
final Label label = createDataThresholdLabel(x, y);
final double Myx = x;
final double Myy = y;
setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (Myx == 0) {
label.setTextFill(Color.DARKGRAY);
} else if (Myx > 0) {
label.setTextFill(Color.SPRINGGREEN);
} else {
label.setTextFill(Color.FIREBRICK);
}
label.setText("Current position: " + Myx + " , " + Myy);
//setCursor(Cursor.NONE);
toFront();
}
});
setOnMouseExited(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
//getChildren().clear();
//setCursor(Cursor.CROSSHAIR);
}
});
}
Now note, I also tried the setStyle(java.lang.String) method, with all of the same type of CSS, with no success. I have NO idea why this isn't styling dynamically. It's almost as if the custom nodes are simply ignoring all new .css that I define at runtime?
Any help would be greatly appreciated, please don't be shy if you need more details or explanation on any points.
So, I did finally find a good workaround to solve my problem, although not in the way I thought it would happen. The main problem I was having, was that I was extending from stackPane to create my node, which only had a very small number of graphical display options available to it, and by switching the 'prefSize()' property, I was simply changing the size of that stackPane, and then filling in the background area of that stack pane black, giving it a very deceptive shape-look to it.
So rather than use a stack pane, whenever I reached the node that I needed to place the red 'X' on, I simply called a different Datum method that returned a datum with an ImageView Attached, like so:
private XYChart.Data CoLDatum(Double x, Double y){
final XYChart.Data data = new XYChart.Data(x, y);
ImageView myImage = new ImageView(new Image(getClass().getResource("style/redX.png").toExternalForm()));
data.setNode(myImage);
data.getNode().setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
main_label.setText("Some Text.");
}
});
data.getNode().setOnMouseExited(new EventHandler<MouseEvent>(){
#Override public void handle(MouseEvent mouseEvent) {
main_label.setText("");
}
});
return data;
}
and since ImageView is an implementing class of Node, this worked out just fine, and allowed me to load up an image for that one single node in the graph, while still maintaining a listener to give custom text to our information label when the red 'x' was hovered over with a mouse. Sometimes, it's the simple solutions that slip right past you.
I imagine that, had I employed stackPane properties properly with the setStyle(java.lang.String) method, they would have absolutely shown up, and I was just butchering the nature of a stack pane. Interesting.
Hopefully this helps somebody else stuck with similar problems!
I'm trying to do something pretty simple. I want to place an icon in a column for a particular row in a table. If it's a folder, display a folder icon. If it's a file, display a file icon.
Does anyone know how to do this in JavaFX 2?
I've tried so many things and this seems like it should be pretty simple or at least an example somewhere.
Okay so I had a huge dummy moment. Turns out that I had my image url path wrong.
I did find a site that provides a great example for adding elements for table. This helped me understand everything.
Now if the 4 different ways I tried before would've worked, I don't know because my image url path was wrong. But anyway here is the link and a code snippet.
Bottom line was that you need to have the CellValueFactory and the CellFactory. I was attempting to use either or. The updateItem template method in TableCell relies on the value dervied from CellValueFactory.
http://blog.ngopal.com.np/2011/10/01/tableview-cell-modifiy-in-javafx/
TableColumn albumArt = new TableColumn("Album Art");
albumArt.setCellValueFactory(new PropertyValueFactory("album"));
albumArt.setPrefWidth(200);
// SETTING THE CELL FACTORY FOR THE ALBUM ART
albumArt.setCellFactory(new Callback<TableColumn<Music,Album>,TableCell<Music,Album>>(){
#Override
public TableCell<Music, Album> call(TableColumn<Music, Album> param) {
TableCell<Music, Album> cell = new TableCell<Music, Album>(){
#Override
public void updateItem(Album item, boolean empty) {
if(item!=null){
HBox box= new HBox();
box.setSpacing(10) ;
VBox vbox = new VBox();
vbox.getChildren().add(new Label(item.getArtist()));
vbox.getChildren().add(new Label(item.getAlbum()));
ImageView imageview = new ImageView();
imageview.setFitHeight(50);
imageview.setFitWidth(50);
imageview.setImage(new Image(MusicTable.class.getResource("img").toString()+"/"+item.getFilename()));
box.getChildren().addAll(imageview,vbox);
//SETTING ALL THE GRAPHICS COMPONENT FOR CELL
setGraphic(box);
}
}
};
System.out.println(cell.getIndex());
return cell;
}
});
In case the provided answers did not work for you (like it didn't for me), this was the solution I found (Of course you still needs to create the tableView and add the columns to it):
//Create your column that will hold the image
private final TreeTableColumn<YourObjectClass,ImageView> columnImage= new TreeTableColumn<YourObjectClass,ImageView>("Image");
public void start() {
//Set your cellValueFactory to a SimpleObjectProperty
//Provided that your class has a method "getImage()" this will work beautifully!
columnImage.setCellValueFactory(c-> new SimpleObjectProperty<ImageView>(new ImageView(c.getValue().getValue().getImage())));
}