I'm currently creating an inventory system in which the user can select an item from their inventory list collection, and the icon on the right will update.
As of now, I've implemented a ClickListener to get the currently selected item from the inventory list and called the .getImage() method that I created to get the icon of the selected item.
I used table.add(image); to add the icon to the table. However, when the icon is added to the table, it fills up that space, and another column is created to the right when the user selects on another item. This is the issue.
How do I get the image area to update when the user selects another item, rather than creating another column to the right?
This is currently how it is: https://imgur.com/a/O6SW8gi
I want the area where the sword is to be updated with the latest item that the user has clicked on.
Here is my code:
package com.sps.game.inventory;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.*;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import com.sps.game.controller.InventoryController;
import com.sps.game.controller.PlayerController;
import java.util.ArrayList;
public class PlayerInventory {
public Stage stage;
public SpriteBatch sb;
private Viewport viewport;
private Skin skin = new Skin(Gdx.files.internal("core/assets/pixthulhuui/pixthulhu-ui.json"));
private List<Item> inventory;
private List<Image> itemImages;
private InventoryController inventoryController;
private InputProcessor oldInput;
Table table = new Table(skin);
public PlayerInventory(SpriteBatch sb, PlayerController playerController) {
this.sb = sb;
viewport = new FitViewport(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), new OrthographicCamera());
stage = new Stage(viewport, sb);
inventoryController = new InventoryController();
inventory = inventoryController.getInventoryList();
// itemImages = inventoryController.getImageList();
}
//THIS IS WHERE THE IMAGE IS ADDED
private void formatting() {
stage = new Stage();
Label inventoryLabel = new Label("Inventory", skin);
final Label imageLabel = new Label("Item", skin);
table.setDebug(true);
table.defaults();
table.center();
table.setFillParent(true);
table.add(inventoryLabel);
table.add(imageLabel);
table.row();
table.add(inventory); //need a way to get the current item selected
inventory.addListener(new ClickListener() {
public void clicked(InputEvent event, float x, float y) {
Item clickedItem = inventory.getSelected();
Image clickedImage = clickedItem.getImage();
table.add(clickedImage);
System.out.println(clickedItem.getName());
}
});
// stage.addActor(itemImages);
stage.addActor(table);
}
public void setInput() {
oldInput = Gdx.input.getInputProcessor(); //Get the old input from the user.
Gdx.input.setInputProcessor(stage); //Set the input to now work on the inventory.
}
public void update() {
if (Gdx.input.isKeyPressed(Input.Keys.I) && oldInput == null) {
formatting();
setInput();
}
if (Gdx.input.isKeyPressed(Input.Keys.O) && oldInput != null) {
stage.dispose();
Gdx.input.setInputProcessor(oldInput);
oldInput = null;
}
}
public void dispose() {
stage.dispose();
}
}
The image is added in the formatting method()-
inventory.addListener(new ClickListener() {
public void clicked(InputEvent event, float x, float y) {
Item clickedItem = inventory.getSelected();
Image clickedImage = clickedItem.getImage();
table.add(clickedImage);
System.out.println(clickedItem.getName());
}
I found the problem, looking into the LibGDX Wiki Table - Adding Cells it seems you are using the add() method that does not replace the previous actor in the cell, it only adds another one in the row. Instead, you should save your displayed image apart from the List
public class PlayerInventory {
[..........................]
Item clickedItem; // This will save your clicked item
Image clickedImage; // This will save your clicked image;
[..........................]
inventory.addListener(new ClickListener() {
public void clicked(InputEvent event, float x, float y) {
if (clickedItem == null && clickedImage == null) {
clickedItem = inventory.getSelected(); // This line changed
clickedImage = clickedItem.getImage(); // This line changed
table.add(clickedImage);
} else {
clickedItem = inventory.getSelected(); // This line changed
clickedImage = clickedItem.getImage(); // This line changed
}
System.out.println(clickedItem.getName());
}
}
This will add the image if there was no image before and replace the image if there was one before. Hope this helps!
Solved. You shouldn't use table.add(clickedImage) every time the screen is clicked. add() creates a new cell. Instead, add a placeholder Image just once during initial layout and keep a reference to it. In my ClickListener, i used clickedImage.setDrawable() instead to change what image is displayed.
Related
I have an 4x4 gridpane object consists of Tiles, extends ImageView, and i want to create method for changing places of connected Tiles by mouse drag and drop. I've figured out how to take first element which drag started but I couldn't get the referances of the ImageView which in drag dropped.
Tile Class
public class Tile extends ImageView {
protected int xCoordinate;
protected int yCoordinate;
protected boolean isMoveable;
protected ArrayList<Point2D> points = new ArrayList<>();
public Tile(int xCoordinate, int yCoordinate) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
super.setFitHeight(125);
super.setFitWidth(125);
}}
GridPane codes
GridPane gridPane = new GridPane();
for (Tile tile : tiles){
gridPane.add(tile,tile.getXCoordinate(),tile.getYCoordinate());
}
StackPane centerPane = new StackPane();
centerPane.setStyle("-fx-background-color: white;");
centerPane.getChildren().add(gridPane);
centerPane.setPadding(new Insets(0,50,0,50));
I have tried this but I don't know how to get referance of connected Tile
gridPane.setOnMouseDragged(e->{
System.out.println(e.getTarget());
gridPane.setOnMouseReleased(e1->{
System.out.println(e1.getTarget());
});
});
I have created the codes for changing places but I should get the referance of the connected Tile when mouse released.
Oracle provides an excellent tutorial on using drag and drop in JavaFX.
You probably want to make use of the Dragboard, which is a special kind of Clipboard.
Points to note
You may not actually need to move the tiles that you have created. They are ImageViews.
You can place the image associated with the source in the dragboard and change the image in the target view when it is dropped.
You can set the dragView on the dragboard for visual feedback of the drag operation.
You can use a custom content type for the dragboard rather than an image, this is explained in the linked Oracle tutorial.
private static final DataFormat customFormat =
new DataFormat("helloworld.custom");
When putting a custom data onto a dragboard, specify the data type. Note that the data must be serializable.
When reading the data from the dragboard, a proper casting is needed.
Potential Approaches
There are two ways you can handle the tile display.
Tile view and model.
Create a separate tile view and tile model interface.
When the tiles change, don't change the view, only change the model instance backing the view. The view observes its model for changes and automatically updates itself. New nodes are not created and existing nodes are not moved.
That is the approach in the example below. The view is an ImageView and the model is an Image.
Encapsulate view and model together.
In this case, you place the information about the model in the view.
When a node is placed in the grid, you record it's grid position, for example by member values in the node or by setting user data on the node.
When a node is dragged to a new position, you query the source node for its position, then you swap the source and target nodes in the grid using:
GridPane.setConstraints(node, columnIndex, rowIndex)
This is essentially the approach you propose in your question.
I do not provide an implementation for this second potential approach.
Example
The images are images of the grid before and after manually dragging the tiles to reorder them.
The example is not going to be exactly what you want, it is purely provided as an example and you will need to adapt it if you wish to make use of some of the concepts in it.
The ImageViewFactory is just to create test images, you can ignore that portion.
Dragging specific stuff is in the DragUtil class.
Code uses Java 18 and newer Java language features, so to compile it you will need to enable the appropriate language level.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.effect.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class TileDragApp extends Application {
private static final String TEXT = "abcdefghijklmnop";
private static final int GRID_SIZE = 4;
private static final int TILE_SIZE = 60;
#Override
public void start(Stage stage) {
ImageViewFactory imageViewFactory = new ImageViewFactory();
ImageView[] imageViews = imageViewFactory.makeImageViews(
TEXT,
TILE_SIZE
);
DragUtil dragUtil = new DragUtil();
GridPane grid = new GridPane();
for (int i = 0; i < TEXT.length(); i++) {
dragUtil.makeDraggable(imageViews[i]);
grid.add(imageViews[i], i % GRID_SIZE, i / GRID_SIZE);
}
grid.setGridLinesVisible(true);
grid.setPadding(new Insets(20));
stage.setScene(new Scene(grid));
stage.setResizable(false);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
class ImageViewFactory {
private static final String CSS =
"""
data:text/css,
""" +
// language=CSS
"""
.root {
-fx-background-color: azure;
}
.label {
-fx-font-size: 40px;
-fx-text-fill: navy;
}
""";
public ImageView[] makeImageViews(String text, int tileSize) {
List<Character> chars =
text.chars()
.mapToObj(
c -> (char) c
).collect(
Collectors.toList()
);
Collections.shuffle(chars);
return chars.stream()
.map(
c -> makeImageView(c, tileSize)
).toArray(
ImageView[]::new
);
}
private ImageView makeImageView(char c, int tileSize) {
Label label = new Label(Character.toString(c));
StackPane layout = new StackPane(label);
layout.setPrefSize(tileSize, tileSize);
Scene scene = new Scene(layout);
scene.getStylesheets().add(CSS);
SnapshotParameters snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.AZURE);
Image image = layout.snapshot(snapshotParameters,null);
return new ImageView(image);
}
}
class DragUtil {
public void makeDraggable(ImageView imageView) {
Effect highlight = createHighlightEffect(imageView);
imageView.setOnDragDetected(e -> {
Dragboard db = imageView.startDragAndDrop(TransferMode.MOVE);
ClipboardContent content = new ClipboardContent();
content.putImage(imageView.getImage());
db.setContent(content);
db.setDragView(makeSmaller(imageView.getImage()));
e.consume();
});
imageView.setOnDragOver(e -> {
if (e.getGestureSource() != imageView
&& e.getDragboard().hasImage()
) {
e.acceptTransferModes(TransferMode.MOVE);
}
imageView.setEffect(highlight);
e.consume();
});
imageView.setOnDragExited(e -> {
imageView.setEffect(null);
e.consume();
});
imageView.setOnDragDropped(e -> {
Dragboard db = e.getDragboard();
boolean success = false;
if (db.hasImage() && e.getGestureSource() instanceof ImageView source) {
source.setImage(imageView.getImage());
imageView.setImage(db.getImage());
success = true;
}
e.setDropCompleted(success);
e.consume();
});
}
private Image makeSmaller(Image image) {
ImageView resizeView = new ImageView(image);
resizeView.setFitHeight(image.getHeight() * 3 / 4);
resizeView.setFitWidth(image.getWidth() * 3 / 4);
SnapshotParameters snapshotParameters = new SnapshotParameters();
return resizeView.snapshot(snapshotParameters, null);
}
private Effect createHighlightEffect(Node n) {
ColorAdjust monochrome = new ColorAdjust();
monochrome.setSaturation(-1.0);
return new Blend(
BlendMode.MULTIPLY,
monochrome,
new ColorInput(
0,
0,
n.getLayoutBounds().getWidth(),
n.getLayoutBounds().getHeight(),
Color.PALEGREEN
)
);
}
}
I'm currently creating an inventory system, in which the user will select an item from a list and the icon on the right will update based on the item that the user has picked.
I need a way to get the list item that the user has currently selected. I then need to use that list item to display an icon which the user will see.
Currently I have tried using the getSelected() method on the inventory list, which seems to be only returning the first item in the list. I need a way to get the item that the user has currently selected.
I need to get the current item selected on the list called 'inventory'.
package com.sps.game.inventory;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.*;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import com.sps.game.controller.InventoryController;
import com.sps.game.controller.PlayerController;
public class PlayerInventory {
public Stage stage;
public SpriteBatch sb;
private Viewport viewport;
private Skin skin = new Skin(Gdx.files.internal("core/assets/pixthulhuui/pixthulhu-ui.json"));
private List<Item> inventory;
private List<Image> itemImages;
private InventoryController inventoryController;
private InputProcessor oldInput;
public PlayerInventory(SpriteBatch sb, PlayerController playerController) {
this.sb = sb;
viewport = new FitViewport(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), new OrthographicCamera());
stage = new Stage(viewport, sb);
inventoryController = new InventoryController();
inventory = inventoryController.getInventoryList();
itemImages = inventoryController.getImageList();
}
private void formatting() {
stage = new Stage();
Label inventoryLabel = new Label("Inventory", skin);
Label imageLabel = new Label("Item", skin);
Table table = new Table(skin);
table.setDebug(true);
table.defaults();
table.center();
table.setFillParent(true);
table.add(inventoryLabel);
table.add(imageLabel);
table.row();
table.add(inventory); //need a way to get the current item selected
table.add(itemImages.getSelected());
stage.addActor(itemImages);
stage.addActor(table);
}
public void setInput() {
oldInput = Gdx.input.getInputProcessor(); //Get the old input from the user.
Gdx.input.setInputProcessor(stage); //Set the input to now work on the inventory.
}
public void update() {
if (Gdx.input.isKeyPressed(Input.Keys.I) && oldInput == null) {
formatting();
setInput();
}
if (Gdx.input.isKeyPressed(Input.Keys.O) && oldInput != null) {
stage.dispose();
Gdx.input.setInputProcessor(oldInput);
oldInput = null;
}
}
public void dispose() {
stage.dispose();
}
}
I have found the solution, through using a Clicklistener.
inventory.addListener(new ClickListener() {
public void clicked(InputEvent event, float x, float y) {
String clickedItem = inventory.getSelected();
table.add(clickedItem);
System.out.println(item.getName());
}
});
I've googled enough but still can find solution to get only single resize event when user releases left mouse button. For example the following solution from here
stage.titleProperty().bind(
scene.widthProperty().asString().
concat(" : ").
concat(scene.heightProperty().asString()));
When user clicks mouse left button and starts resizing the stage we will get very many events (using property listeners) while he does resizing. However, I want to get only one event - when the user completes resizing and releases mouse left button.
Another solution is here This solution significantly decreases amount of events but still doesn't let to get only one.
How to get only one resize event after user releases mouse button?
As far as I know, the mouse event handlers that resize the stage are managed natively, and so there is no way to access those purely in JavaFX - to do this the way you describe would require writing native libraries and hooking into them.
If you are doing some heavy computation (or other work that takes a long time) in response to the change in size of the stage, your best bet is probably to write code that only processes one change at a time, and just processes the last known change when it can.
An example of this is:
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class StageResizeThrottling extends Application {
private Random rng = new Random();
#Override
public void start(Stage primaryStage) {
BlockingQueue<Point2D> dimensionChangeQueue = new ArrayBlockingQueue<>(1);
ChangeListener<Number> dimensionChangeListener = (obs, oldValue, newValue) -> {
dimensionChangeQueue.clear();
dimensionChangeQueue.add(new Point2D(primaryStage.getWidth(), primaryStage.getHeight()));
};
primaryStage.widthProperty().addListener(dimensionChangeListener);
primaryStage.heightProperty().addListener(dimensionChangeListener);
Thread processDimensionChangeThread = new Thread(() -> {
try {
while (true) {
System.out.println("Waiting for change in size");
Point2D size = dimensionChangeQueue.take();
System.out.printf("Detected change in size to [%.1f, %.1f]: processing%n", size.getX(), size.getY());
process(size, primaryStage);
System.out.println("Done processing");
}
} catch (InterruptedException letThreadExit) { }
});
processDimensionChangeThread.setDaemon(true);
processDimensionChangeThread.start();
Scene scene = new Scene(new StackPane(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void process(Point2D stageDimension, Stage stage) throws InterruptedException {
// simulate slow process:
Thread.sleep(500 + rng.nextInt(1000));
final String title = String.format("Width: %.0f Height: %.0f", stageDimension.getX(), stageDimension.getY());
Platform.runLater(() -> stage.setTitle(title));
}
public static void main(String[] args) {
launch(args);
}
}
Note that this will always process the very first change immediately, and then process the latest change when each previously-processed change has finished processing. If no further changes have occurred, it will wait until one does occur and then process it immediately. If you like, you can combine this with the timer-based technique you linked for coalescing the changes in the listener, which will typically remove the very first change that is processed (which is usually redundant as it is almost always followed by subsequent changes). The following changes will wait until no resizes have occurred for 300ms before submitting one to the queue for processing (the thread still behaves the same way - it will process the latest change, and when that processing is complete, wait for another one):
BlockingQueue<Point2D> dimensionChangeQueue = new ArrayBlockingQueue<>(1);
PauseTransition coalesceChanges = new PauseTransition(Duration.millis(300));
coalesceChanges.setOnFinished(e -> {
dimensionChangeQueue.clear();
dimensionChangeQueue.add(new Point2D(primaryStage.getWidth(), primaryStage.getHeight()));
});
ChangeListener<Number> dimensionChangeListener = (obs, oldValue, newValue) ->
coalesceChanges.playFromStart();
primaryStage.widthProperty().addListener(dimensionChangeListener);
primaryStage.heightProperty().addListener(dimensionChangeListener);
There's some tuning here, which is a tradeoff between latency and over-eagerness in processing changes. You probably want the pause transition to last something shorter than the average processing time of the change in screen size, but not an order of magnitude shorter.
The code guarantees that no more than one change will be processed at a time and that the latest change will eventually be processed if no more changes occur. This is probably about as good as you can get without accessing native user events. (And it would also handle programmatic changes in the stage size, which a mouse handler would not handle.)
I tried to create an example to achieve what you are looking for, I ended up with this, it is not perfect but when I tested it, it looked like it could help:
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class OneEventJavaFX extends Application{
double originalWidth = 400; // the initial width of Scene when the program starts
double originalHeight = 400; // the initial height of Scene when the program starts
// boolean property to be observed in order to know the completion of stage resize
BooleanProperty completedProperty = new SimpleBooleanProperty(false);
Timeline timeline;
#Override
public void start(Stage stage) throws Exception {
Pane root = new Pane(); // simple root as example just for test purpose
Scene scene = new Scene(root, 400,400);
stage.setScene(scene);
stage.setTitle("OneEventJavaFX");
stage.show();
// because I could not find a way to implement MouseEvent.MOUSE_RELEASED
// on the stage to notify the completion on resizing, I had to use a TimeLine
// the duration should consider the time the user usually take to finish every resize
// duration is tricky, Very Slow Resizing V.S Very Fast Resizing!
timeline = new Timeline(new KeyFrame(Duration.seconds(1), e ->{
System.out.println("Resizing Should Be Completed By Now!");
originalWidth = scene.getWidth(); // record the new scene size
originalHeight = scene.getHeight();
completedProperty.setValue(false);
}));
// change listener, to be added to and removed from the scene
ChangeListener<Number> changeListener= (observable, oldValue, newValue) ->{
System.out.println("I am Detecting an Event!"); // test
// once the size changed
if(originalWidth-scene.getWidth()>1 || scene.getWidth()-originalWidth>1 ||
originalHeight-scene.getHeight()>1 || scene.getHeight()-originalHeight>1){
completedProperty.set(true); // notify that completion should be considered
System.out.println("I Stopped! No More Events!");
timeline.play(); // and start counting the time
}};
// add the change listener when the program starts up
scene.widthProperty().addListener(changeListener);
scene.heightProperty().addListener(changeListener);
System.out.println("ChangeListener Added At Startup!");
// now listen to the change of the boolean property value
// instead of the size changes, it should NOT take a lot of work
// then accordingly add and remove change listener!
completedProperty.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean notComplete, Boolean complete) {
if (complete) {
scene.widthProperty().removeListener(changeListener);
scene.heightProperty().removeListener(changeListener);
System.out.println("ChangeListener Removed!");
}
else{
scene.widthProperty().addListener(changeListener);
scene.heightProperty().addListener(changeListener);
System.out.println("ChangeListener Added Back!");
}
}
});
}
public static void main(String[] args) {
launch();
}
}
Test While Resizing
ChangeListener Added At Startup!
I am Detecting an Event!
I am Detecting an Event!
ChangeListener Removed!
I Stopped! No More Events!
Resizing Should Be Completed By Now!
ChangeListener Added Back!
UPDATE:
I have been working on solving this question, I believe this approach can achieve what you want.
The idea is as follows:
Create UNDECORATED Stage and Make it Resizable.
Create a Title Bar and add it to the Stage.
Now the Mouse Events can be detected on the Border of the Stage (because basically it happens on the Scene).
Create Double Property for both the Width and Height of Stage and add Change Listener to listen to the Changes.
The changes in the Stage Width & Height will only be recorded at the beginning of the drag and when user RELEASES the Mouse.
Explanations in Comments.
The whole solution can be found here as an archive file (Why? Because I tried to post it here fully but the Body Limit is 30000 Character!) .
OneEventStage Class:
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* This class customize a given Stage to record the changes
* of its size only when user starts and finishes resizing (recording one event)
* #author Yahya Almardeny
* #version 28/05/2017
*/
public class OneEventStage{
private double originalWidth; // the initial width of Scene when the program starts
private double originalHeight; // the initial height of Scene when the program starts
private TitleBar titleBar; // can be customized by the setter method (by default I made it for Windows 10 style)
private boolean started, alreadyFullScreen;
private DoubleProperty widthChange, heightChange; // record the changes in size
public Scene s;
public BorderPane scene; // this will be considered as a Scene when used in the program
public OneEventStage(Stage stage, double width, double height){
originalWidth = width; originalHeight = height;
widthChange = new SimpleDoubleProperty(originalWidth);
heightChange = new SimpleDoubleProperty(originalHeight);
started = false;
titleBar = new TitleBar("");
scene = new BorderPane();
scene.setTop(titleBar.getTitleBar());
s = new Scene(scene, originalWidth,originalHeight);
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(s);
ResizeHelper.addResizeListener(stage);
Task<Void> task = new Task<Void>(){
#Override
protected Void call() throws Exception {
Platform.runLater(new Runnable(){
#Override
public void run() {
// change listener, to be added to and removed from the scene
ChangeListener<Number> changeListener= (observable, oldValue, newValue) ->{
if(isFullScreen()){
widthChange.setValue(stage.getWidth());
heightChange.setValue(stage.getHeight());
alreadyFullScreen=true;
}
else if (alreadyFullScreen){ // coming from full screen mode
widthChange.setValue(Screen.getPrimary().getVisualBounds().getWidth());
heightChange.setValue(Screen.getPrimary().getVisualBounds().getHeight());
widthChange.setValue(originalWidth);
heightChange.setValue(originalHeight);
alreadyFullScreen = false;
}
else if(!alreadyFullScreen && !started){
started = true; // to inform the detecting Mouse Release Event is required
}
};
s.setOnMouseReleased(e->{
if(started){ // if this happens particularly after changing the size/dragging
originalWidth = stage.getWidth(); // record the new scene size
originalHeight = stage.getHeight();
widthChange.setValue(originalWidth); // add it
heightChange.setValue(originalHeight);
started = false;
}
});
// add the change listener when the program starts up
s.widthProperty().addListener(changeListener);
s.heightProperty().addListener(changeListener);
}
});
return null;
}};
new Thread(task).start();
}
/*
* to detected if user clicked on maximize button or double click on the title bar
*/
private boolean isFullScreen(){
return this.s.getWindow().getWidth()==Screen.getPrimary().getVisualBounds().getWidth() &&
this.s.getWindow().getHeight()==Screen.getPrimary().getVisualBounds().getHeight();
}
public DoubleProperty getWidthChange() {
return widthChange;
}
public DoubleProperty getHeightChange() {
return heightChange;
}
public TitleBar getTitleBar() {
return titleBar;
}
public void setTitleBar(TitleBar titleBar) {
this.titleBar = titleBar;
}
public void setTitle(String title){
titleBar.getTitle().setText(title);
}
}
OneEventStageTest Class:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* Implementing an Example of OneEventStage to test it
* #author Yahya Almardeny
* #version 28/05/2017
*/
public class OneEventStageTest extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
// create stage
OneEventStage stage = new OneEventStage(primaryStage, 400,400);
stage.setTitle("One Event Stage");
// simple containers and its components for testing purpose
VBox container = new VBox();
container.setAlignment(Pos.CENTER);
HBox widthInfoContainer = new HBox();
widthInfoContainer.setAlignment(Pos.CENTER);
Label widthChangeL = new Label("Width Changes");
TextField widthChangeV = new TextField();
widthChangeV.setEditable(false);
widthInfoContainer.getChildren().addAll(widthChangeL, widthChangeV);
HBox.setMargin(widthChangeL, new Insets(10));
HBox.setMargin(widthChangeV, new Insets(10));
HBox heightInfoContainer = new HBox();
heightInfoContainer.setAlignment(Pos.CENTER);
Label heightChangeL = new Label("Height Changes");
TextField heightChangeV = new TextField();
heightChangeV.setEditable(false);
heightInfoContainer.getChildren().addAll(heightChangeL, heightChangeV);
HBox.setMargin(heightChangeL, new Insets(10));
HBox.setMargin(heightChangeV, new Insets(10));
container.getChildren().addAll(widthInfoContainer, heightInfoContainer);
//////////////////////////////////////////////////////////////////////////
DoubleProperty widthChange = stage.getWidthChange();
DoubleProperty heightChange = stage.getHeightChange();
// listen to the changes (Testing)
widthChange.addListener((obs, old, newV)->{
Platform.runLater(new Runnable(){
#Override
public void run() {
widthChangeV.setText("From(" + old.doubleValue() + ") To(" + newV.doubleValue() + ")");
}
});
});
heightChange.addListener((obs, old, newV)->{
Platform.runLater(new Runnable(){
#Override
public void run() {
heightChangeV.setText("From(" + old.doubleValue() + ") To(" + newV.doubleValue() + ")");
}
});
});
//////////////////////////////////////////////////////////////////////////////////////
// represent a root but in fact it's inside the real root (BorderPane in the OneEventStage Class!).
StackPane root = new StackPane();
root.setAlignment(Pos.CENTER);
root.getChildren().add(container);
stage.scene.setCenter(root);
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}
TitleBar Class:
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.stage.Screen;
import javafx.stage.Stage;
/**
* This class to create a default/customized Title Bar
* to be added to Undecorated Stage in JavaFX Application
* #author Yahya Almardeny
* #version 27/05/2017
*/
public class TitleBar {
private HBox titleBar;
private ImageView icon;
private StackPane close, minimize, maximize; // represent customized components for the title bar (by using the second constructor)
private Image maximizeBefore, maximizeAfter; // for changing maximize icon when it's full screen
private Label title;
private double height, stageWidth, stageHeight, x,y, offsetX, offsetY;
private double screenWidth = Screen.getPrimary().getVisualBounds().getWidth(),
screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
private Color backgroundColor;
private StackPane maximizeButton; // for default title bar
private Label minimizeButton, closeButton; // for default title bar
private Stage stage;
private boolean intialized = false, fromMax = false;
public static enum Components {ICON,TITLE,MINIMIZE,MAXIMIZE,CLOSE;}
/**
* the default constructor, appearance of Windows 10
* #param title
*/
public TitleBar(String title){
titleBar = new HBox();
icon = new ImageView(new Image(TitleBar.class.getResourceAsStream("/icon/icon.png")));
icon.setFitWidth(15); this.icon.setFitHeight(13);
closeButton = new Label("×");
closeButton.setFont(Font.font("Times New Roman", 25));
closeButton.setPrefWidth(46);
closeButton.setAlignment(Pos.CENTER);
minimizeButton = new Label("—");
minimizeButton.setFont(Font.font(10));
minimizeButton.setPrefWidth(46);
minimizeButton.setPrefHeight(29);
minimizeButton.setAlignment(Pos.CENTER);
maximizeButton = maximiazeButton();
this.title = new Label(title);
final Pane space = new Pane();
HBox.setHgrow(space,Priority.ALWAYS);
titleBar.getChildren().addAll(this.icon, this.title,space,this.minimizeButton, this.maximizeButton, this.closeButton);
titleBar.setAlignment(Pos.CENTER_RIGHT);
HBox.setMargin(this.icon, new Insets(0,5,0,10)); // top,right, bottom, left
initalize(); // private method to get the Stage for first time
setDefaultControlsFunctionality(); // private method to add the default controls functionality
}
/**
* This is constructor to create a custom title bar
* #param icon
* #param minimize
* #param maximize
* #param close
* #param title
*/
public TitleBar(Image icon, Image minimize, Image maximizeBefore, Image maximizeAfter, Image close, String title){
titleBar = new HBox();
this.icon = new ImageView(icon);
this.icon.setFitWidth(15); this.icon.setFitHeight(14); // values can be changed via setters
this.close = new StackPane();
this.close.setPrefSize(25, 20);
this.close.getChildren().add(new ImageView(close));
((ImageView) this.close.getChildren().get(0)).setFitWidth(20);
((ImageView) this.close.getChildren().get(0)).setFitHeight(20);
this.minimize = new StackPane();
this.minimize.setPrefSize(25, 20);
this.minimize.getChildren().add(new ImageView(minimize));
((ImageView) this.minimize.getChildren().get(0)).setFitWidth(20);
((ImageView) this.minimize.getChildren().get(0)).setFitHeight(20);
this.maximizeBefore = maximizeBefore;
this.maximize = new StackPane();
this.maximize.setPrefSize(25, 20);
this.maximize.getChildren().add(new ImageView(maximizeBefore));
((ImageView) this.maximize.getChildren().get(0)).setFitWidth(20);
((ImageView) this.maximize.getChildren().get(0)).setFitHeight(20);
this.maximizeAfter = maximizeAfter;
this.title = new Label(title);
final Pane space = new Pane();
HBox.setHgrow(space,Priority.ALWAYS);
titleBar.getChildren().addAll(this.icon, this.title,space,this.minimize, this.maximize, this.close);
titleBar.setAlignment(Pos.CENTER_RIGHT);
HBox.setMargin(this.icon, new Insets(0,5,0,10)); // top,right, bottom, left
HBox.setMargin(this.close, new Insets(0,5,0,0));
initalize();
setCustomizedControlsFunctionality();
}
/**
* create the default maximize button
* #return container
*/
private StackPane maximiazeButton(){
StackPane container = new StackPane();
Rectangle rect = new Rectangle(8,8);
rect.setFill(Color.TRANSPARENT);
rect.setStroke(Color.BLACK);
container.setPrefWidth(46);
container.getChildren().add(rect);
return container;
}
/**
* To get the Stage of the application for one time only
* as well as adding listener to iconifiedProperty()
*/
private void initalize(){
titleBar.setOnMouseEntered(e->{ // the entire block will be executed only once
if(!intialized){
// get the stage and assign it to the Stage field
stage = ((Stage)titleBar.getScene().getWindow());
// add listener toiconifiedProperty()
stage.iconifiedProperty().addListener(ee->{
if(!stage.isIconified()){
stage.setMaximized(true);
if(fromMax){ // if already maximized
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.setX(0);
stage.setY(0);
}
else{
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
}
try { // to remove the flash
Thread.sleep(10);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
stage.setOpacity(1.0);
}
});
intialized=true;
}
});
}
/**
* To add functionality to title bar controls
* via event listeners
*/
private void setDefaultControlsFunctionality(){
// Double-Click on Title Bar
titleBar.setOnMouseClicked(e->{
if(e.getClickCount()==2){
maximizefunctonality();
}
});
//Maximize Control
maximizeButton.setOnMouseEntered(e->{// highlight when hover
maximizeButton.setBackground(
new Background(new BackgroundFill(Color.LIGHTGRAY,null,null)));
((Rectangle)maximizeButton.getChildren().get(0)).setFill(Color.LIGHTGRAY);
if(maximizeButton.getChildren().size()==2){
((Rectangle)maximizeButton.getChildren().get(1)).setFill(Color.LIGHTGRAY);
}
});
maximizeButton.setOnMouseExited(e->{ // remove highlight
maximizeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
((Rectangle)maximizeButton.getChildren().get(0)).setFill(Color.TRANSPARENT);
if(maximizeButton.getChildren().size()==2){
((Rectangle)maximizeButton.getChildren().get(1)).setFill(Color.WHITE);
}
});
maximizeButton.setOnMouseClicked(e->{
maximizefunctonality();
});
//Close Control
closeButton.setOnMouseEntered(e->{
closeButton.setBackground(
new Background(new BackgroundFill(Color.CRIMSON,null,null)));
closeButton.setTextFill(Color.WHITE);
});
closeButton.setOnMouseExited(e->{
closeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
closeButton.setTextFill(Color.BLACK);
});
closeButton.setOnMouseClicked(e->{
stage.close();
});
//Minimize Control
minimizeButton.setOnMouseEntered(e->{
minimizeButton.setBackground(
new Background(new BackgroundFill(Color.LIGHTGRAY,null,null)));
});
minimizeButton.setOnMouseExited(e->{
minimizeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
});
minimizeButton.setOnMouseClicked(e->{
if(!stage.isIconified()){ // if it's not minimized
if(fromMax){ // check if it's already full screen(maximized)
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
else{ // if it's not -> record the size and position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
}
});
// to make title bar movable
titleBar.setOnMousePressed(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
offsetX = e.getScreenX() - stage.getX();
offsetY = e.getScreenY() - stage.getY();
}
});
titleBar.setOnMouseDragged(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
stage.setX(e.getScreenX() - offsetX);
stage.setY(e.getScreenY() - offsetY);
}
});
}
private void maximizefunctonality(){
Rectangle rect = (Rectangle) maximizeButton.getChildren().get(0);
if(stage.getWidth()<screenWidth||stage.getHeight()<screenHeight){
// get the previous size + position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
// maximize it
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.centerOnScreen();
// change the maximize button appearance
rect.setTranslateX(2);
rect.setTranslateY(-2);
Rectangle rect1 = new Rectangle(8,8);
rect1.setFill(Color.WHITE);
rect1.setStroke(Color.BLACK);
maximizeButton.getChildren().add(rect1);
fromMax = true;
}
else{ // if already maximized -> return to previous size + position
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
fromMax = false;
// change the maximize button appearance
rect.setTranslateX(0);
rect.setTranslateY(0);
maximizeButton.getChildren().remove(1);
}
}
private void setCustomizedControlsFunctionality(){
//Maximize Control
maximize.setOnMouseClicked(e->{
if(stage.getWidth()<screenWidth||stage.getHeight()<screenHeight){
// get the previous size + position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
// maximize it
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.centerOnScreen();
// change the maximize button appearance
((ImageView) maximize.getChildren().get(0)).setImage(maximizeAfter);
fromMax = true;
}
else{ // if already maximized -> return to previous size + position
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
fromMax = false;
// change the maximize button appearance
((ImageView) maximize.getChildren().get(0)).setImage(maximizeBefore);
}
});
close.setOnMouseClicked(e->{
stage.close();
});
//Minimize Control
minimize.setOnMouseClicked(e->{
if(!stage.isIconified()){ // if it's not minimized
if(fromMax){ // check if it's already full screen(maximized)
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
else{ // if it's not -> record the size and position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
}
});
// to make title bar movable
titleBar.setOnMousePressed(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
offsetX = e.getScreenX() - stage.getX();
offsetY = e.getScreenY() - stage.getY();
}
});
titleBar.setOnMouseDragged(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
stage.setX(e.getScreenX() - offsetX);
stage.setY(e.getScreenY() - offsetY);
}
});
}
/**
* To change margins/insets to the Title Bar components
* #param component
* #param top
* #param right
* #param bottom
* #param left
*/
public void setInsets(Components component, double top, double right, double bottom, double left){
switch(component){
case TITLE:
HBox.setMargin(title, new Insets(top, right, bottom ,left));
break;
case ICON:
HBox.setMargin(icon, new Insets(top, right, bottom ,left));
break;
case CLOSE:
HBox.setMargin(close, new Insets(top, right, bottom ,left));
break;
case MAXIMIZE:
HBox.setMargin(maximize, new Insets(top, right, bottom ,left));
break;
case MINIMIZE:
HBox.setMargin(minimize, new Insets(top, right, bottom ,left));
break;
}
}
public void setControlsSpace(Components component, double width, double height){
switch(component){
case CLOSE:
close.setPrefSize(width, height);
break;
case MAXIMIZE:
maximize.setPrefSize(width, height);
break;
case MINIMIZE:
minimize.setPrefSize(width, height);
break;
case TITLE:
//do nothing
break;
case ICON:
// do nothing
break;
}
}
public void addHoverEffect(Components component, Color defaultColor, Color onHover, Cursor cursor){
}
//reset of the class
{...}
}
ResizeHelper Class:
{....}
Test
At work I created a TableView that needs to have specific cells flash from one color to the other simultaneously. This is relatively easy using Rectangles, FillTransitions, and a ParallelTransition as shown in the toy example below. After assigning the rectangle to the FillTransition, I set the TableCell's graphic to the rectangle. Then I just had to add/remove the FillTransition from the ParallelTransition depending on whether or not the cell should be blinking.
An area where I had a lot of difficulty, however, was in figuring out a way to scale the rectangle to the size of the TableCell containing it as a graphic. The problem I had was that the TableCell would always resize itself to have empty space between its boundaries and the boundaries of the rectangle.
I had to solve this in a very tedious and round-about way: I had to call setFixedCellSize to fix the table's cell height to whatever my rectangle's height was, reposition the rectangle up and to the left by trial and error through calling its setTranslateX/Y, and set the minwidth and minheight of the column to slightly less than whatever my rectangle's width and height was set to. It solved the problem, but I would've hoped for something a little less tedious and annoying.
I would have assumed this could be avoided by doing one or more of the following with the cell:
Calling setScaleShape(true)
Calling setContentDisplay(ContentDisplay.GRAPHIC_ONLY)
Setting the cell's CSS style to include "-fx-scale-shape: true"
Sadly, none of these had any noticeable effect...
My question is a three-parter:
Is there a better way to size a Shape assigned as a graphic for a Cell to fill the boundaries of the Cell?
Why would none of the three methods above have any effect in my case and what is their actual intended purpose? Do they only apply for a shape assigned with setShape() as opposed to setGraphic()?
Are there any legitimate reasons why JavaFx wouldn't support setting the preferred width or height of Nodes other than those that subclass Region? Autosizing seems like something that should be universal to all Nodes in the hierarchy, and it seems intuitive that any Parent node should be able to dictate the size of its children when necessary.
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javafx.animation.Animation;
import javafx.animation.FillTransition;
import javafx.animation.ParallelTransition;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class FlashingPriorityTable extends Application {
public static void main(String args[]) {
FlashingPriorityTable.launch();
}
#Override
public void start(Stage primaryStage) throws Exception {
// periodically add prioritized items to an observable list
final ObservableList<PItem> itemList = FXCollections.observableArrayList();
class ItemAdder {
private int state, count = 0; private final int states = 3;
public synchronized void addItem() {
state = count++ % states;
PItem item;
if(state == 0)
item = new PItem(Priority.LOW, count, "bob saget");
else if(state == 1)
item = new PItem(Priority.MEDIUM, count, "use the force");
else
item = new PItem(Priority.HIGH, count, "one of us is in deep trouble");
itemList.add(item);
}
};
final ItemAdder itemAdder = new ItemAdder();
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
() -> itemAdder.addItem(),
0, // initial delay
1, // period
TimeUnit.SECONDS); // time unit
// set up a table view bound to the observable list
final TableColumn<PItem, Priority> priCol = new TableColumn<>("Priority");
priCol.setCellValueFactory(new PropertyValueFactory<PItem, Priority>("priority"));
priCol.setCellFactory((col) -> new PriorityCell()); // create a blinking cell
priCol.setMinWidth(50);
priCol.setMaxWidth(50);
final TableColumn<PItem, Integer> indexCol = new TableColumn<>("Index");
indexCol.setCellValueFactory(new PropertyValueFactory<PItem, Integer>("index"));
indexCol.setCellFactory((col) -> makeBorderedTextCell());
final TableColumn<PItem, String> descriptionCol = new TableColumn<>("Description");
descriptionCol.setCellValueFactory(new PropertyValueFactory<PItem, String>("description"));
descriptionCol.setCellFactory((col) -> makeBorderedTextCell());
descriptionCol.setMinWidth(300);
final TableView<PItem> table = new TableView<>(itemList);
table.getColumns().setAll(priCol, indexCol, descriptionCol);
table.setFixedCellSize(25);
// display the table view
final Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
// render a simple cell text and border
private <T> TableCell<PItem, T> makeBorderedTextCell() {
return new TableCell<PItem, T>() {
#Override protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if(item == null || empty) {
setText(null);
} else {
setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, null, null)));
setText(item.toString());
}
}
};
}
/* for cells labeled as high priority, render an animation that blinks (also include a border) */
public static class PriorityCell extends TableCell<PItem, Priority> {
private static final ParallelTransition pt = new ParallelTransition();
private final Rectangle rect = new Rectangle(49.5, 24);
private final FillTransition animation = new FillTransition(Duration.millis(100), rect);
public PriorityCell() {
rect.setTranslateX(-2.75);
rect.setTranslateY(-2.7);
animation.setCycleCount(Animation.INDEFINITE); animation.setAutoReverse(true); }
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
if(priority == null || empty) {
setGraphic(null);
return;
}
setGraphic(rect);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, null, null)));
if(priority == Priority.HIGH) {
if(!pt.getChildren().contains(animation)) {
animation.setFromValue(Color.BLACK);
animation.setToValue(priority.getColor());
animation.setShape(rect);
pt.getChildren().add(animation);
pt.stop(); pt.play();
}
} else {
if(pt.getChildren().contains(animation)) {
pt.getChildren().remove(animation);
pt.stop(); pt.play();
}
rect.setFill(priority.getColor());
}
}
}
/* an item that has a priority assigned to it */
public static class PItem {
private ObjectProperty<Priority> priority = new SimpleObjectProperty<>();
private IntegerProperty index = new SimpleIntegerProperty();
private StringProperty description = new SimpleStringProperty();
public PItem(Priority priority, Integer index, String description) {
setPriority(priority); setIndex(index); setDescription(description);
}
public void setPriority(Priority priority_) { priority.set(priority_); }
public Priority getPriority() { return priority.get(); }
public void setIndex(int index_) { index.set(index_); }
public Integer getIndex() { return index.get(); }
public void setDescription(String description_) { description.set(description_); }
public String getDescription() { return description.get(); }
}
/* a priority */
public enum Priority {
HIGH(Color.RED), MEDIUM(Color.ORANGE), LOW(Color.BLUE);
private final Color color;
private Priority(Color color) { this.color = color; }
public Color getColor() { return color; }
}
}
Regarding:
the TableCell would always resize itself to have empty space between its boundaries and the boundaries of the rectangle.
This is because the cell has by default 2 px of padding, according to modena.css:
.table-cell {
-fx-padding: 0.166667em; /* 2px, plus border adds 1px */
-fx-cell-size: 2.0em; /* 24 */
}
One easy way to get rid of this empty space is just override it:
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
...
setGraphic(rect);
setStyle("-fx-padding: 0;");
...
}
The next problem you also mention is autosizing. According to JavaDoc, for Node.isResizable():
If this method returns true, then the parent will resize the node (ideally within its size range) by calling node.resize(width,height) during the layout pass. All Regions, Controls, and WebView are resizable classes which depend on their parents resizing them during layout once all sizing and CSS styling information has been applied.
If this method returns false, then the parent cannot resize it during layout (resize() is a no-op) and it should return its layoutBounds for minimum, preferred, and maximum sizes. Group, Text, and all Shapes are not resizable and hence depend on the application to establish their sizing by setting appropriate properties (e.g. width/height for Rectangle, text on Text, and so on). Non-resizable nodes may still be relocated during layout.
Clearly, a Rectangle is not resizable, but this doesn't mean you can't resize it: if the layout doesn't do it for you, you'll need to take care of it.
So one easy solution may be binding the dimensions of the rectangle to those of the cell (minus 2 pixels for the cell borders):
private final Rectangle rect = new Rectangle();
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
if(priority == null || empty) {
setGraphic(null);
return;
}
setGraphic(rect);
setStyle("-fx-padding: 0;");
rect.widthProperty().bind(widthProperty().subtract(2));
rect.heightProperty().bind(heightProperty().subtract(2));
...
}
Note that you won't need to translate the rectangle, and it won't be necessary to fix the size of the cell nor the width of the column, unless you want to give it a fixed size.
Note also that setShape() is intended to change the cell shape, that by default is already a rectangle.
This may answer your first two questions. For the third one, sometimes you wish the nodes were always resizable... but if that were the case we will have the opposite problem, trying to keep them constrained...
I'm currently trying to add some images from a decoded video to a TableView row and they are not appearing. Only empty TableColumns. The TableView has been designed in JavaFx Scene Builder along with the Label.
Here's what I got so far:
public class MainScreenController implements Initializable {
#FXML
private Label previewBoxLabel;
#FXML
private TableView tableView;
private ObservableList<ImageView> imageList = FXCollections.observableArrayList();
#FXML
public void AddClipBeta(){
//Code which uses an external class in order to decode video (Variables Frames, width and height are not shown but are present in the actual code)
VideoSegment clip = new VideoSegment(0, file.getPath(), 0, Frames, width, height);
//Opens the file in decoding class - ready to output frames
try{clip.openFile();} catch(Exception e){}
//First frame is updated on the preview box
previewBoxLabel.setGraphic(new ImageView(convertToFxImage(clip.getThumbnail())));
System.out.println(file.getPath());
int i =0;
//While loop in test phase to see whether or not 10 frames will be visible in the table
while(i != 10){
//Creates and sets columns to tableView
TableColumn<ImageView, ImageView> col = new TableColumn<ImageView, ImageView>();
col.setPrefWidth(100); //Set width of column
tableView.getColumns().add(col);
col.setCellFactory(new Callback<TableColumn<ImageView, ImageView>, TableCell<ImageView, ImageView>>() {
#Override
public TableCell<ImageView, ImageView> call(TableColumn<ImageView, ImageView> p) {
TableCell<ImageView, ImageView> cell = new TableCell<ImageView, ImageView>(){
};
return cell;
}
});
//Adds current frame to list
imageList.add(new ImageView(convertToFxImage(clip.getThumbnail())));
//Gets next video frame
try{clip.getNextFrame();} catch(Exception e){}
//Updates counter
i++;
}
//Sets list of frames on the table
tableView.setItems(imageList);
}
// There is a problem with this implementation: transparent pixels on the BufferedImage aren't converted to transparent pixels on the fxImage.
public static javafx.scene.image.Image convertToFxImage(java.awt.image.BufferedImage awtImage) {
if (Image.impl_isExternalFormatSupported(BufferedImage.class)) {
return javafx.scene.image.Image.impl_fromExternalImage(awtImage);
} else {
return null;
}
}
I've been struggling understanding how the TableView works the last couple of days and it would be a real breakthrough if we could get to the bottom of this.
Thanks for reading and any help in advance!
When setting a CellFactory, you need to take in to account that it will override some default bevaiours such as setting text and images.
For example. I had to create a ListView of Applications that launched on double click. I had to set a CellFactory in order to add a listener to the mouse click of each individual cell.
applications.setCellFactory(new Callback<TreeView<Application>, TreeCell<Application>>() {
#Override
public TreeCell<Application> call(TreeView<Application> param) {
return new TreeCell<Application>() {
#Override
protected void updateItem(Application item, boolean empty) {
//call the origional update first
super.updateItem(item, empty);
//the root item in my list is null, this check is required to keep a null pointer from happening
if (item != null) {
// text and graphic are stored in the Application object and set.
this.setText(item.getApplicationListName());
this.setGraphic(item.getGraphic());
// registers the mouse event to the cell.
this.setOnMouseClicked((MouseEvent e) -> {
if (e.getClickCount() == 2) {
try {
this.getItem().launch(tabBar);
} catch (UnsupportedOperationException ex) {
Dialogs.create().nativeTitleBar().masthead("Comming Soon™").message("Application is still in development and will be available Soon™").nativeTitleBar().title("Unavailable").showInformation();
}
} else {
e.consume();
}
});
}else if(empty){
this.setText(null);
this.setGraphic(null);
this.setOnMouseClicked(null);
}
}
};
}
});
This was pieced together from some other code so if there is anything else you would like explained, let me know!
I managed to sort this out with the help of you guys. Basically, what I did was make a class with a bunch of setters and getters and a constructor that takes in ImageViews and sets it to a variable in the class via it's constructors. Then I went back to my code and added the following:
Class with Getters and Setters:
import javafx.scene.image.ImageView;
public class tableDataModel {
private ImageView image;
public tableDataModel(ImageView image){
this.image = image;
}
public ImageView getImage(){
return image;
}
public void setImage(ImageView image){
this.image = image;
}
}
Code from MainScreenController:
TableColumn<tableDataModel, ImageView> col = new TableColumn<>();
tableView.getColumns().add(col);
imageList.add(new tableDataModel(new ImageView(convertToFxImage(clip.getThumbnail()))));
col.setPrefWidth(50);
col.setCellValueFactory(new PropertyValueFactory<tableDataModel, ImageView>("image"));
int i = 0;
while (i != 10) {
try {
imageList.add(new tableDataModel(new ImageView(convertToFxImage(clip.getNextFrame()))));
} catch (Exception e) {
}
i++;
}
tableView.setItems(imageList);