I have a custom object hold by a ObjectProperty instance which should be bound to the StringProperty of a javafx.scene.text.Text.
If I do the obvious thing and use text.textProperty().bind(..); the object property gets bound and the Text actually displays content (I believe the result of toString).
But I do need to modify the String which is actually displayd in text.
Where can I modify what value is actually provided to the binding?
== EDIT ==
Following the first anwer I created this simple test application:
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class BindingsTest {
public static void main(String... args) {
final ObjectProperty<Foo> foo = new SimpleObjectProperty<>();
foo.set(Foo.FOO);
final StringProperty textProperty = new SimpleStringProperty();
textProperty.bind(Bindings.createStringBinding(() -> foo.get().name().toLowerCase()));
System.out.println(textProperty.get());
foo.set(Foo.BAR);
System.out.println(textProperty.get());
}
private enum Foo {
FOO, BAR
}
}
Both outputs are 'foo' whereas I expected the second one to be 'bar'. So after all probably Bindings.createStringBinding(..) is not what I am looking for?
You are looking for Bindings.createStringBinding().
final ObjectProperty<CustomObject> objProperty;
text.textProperty.bind(Bindings.createStringBinding(() -> {
final CustomObject value = objProperty.getValue();
return value != null ? value.toString().toUpperCase() : "";
}, objProperty));
Related
When you look docs about what returns a given CSSMetadata (getCSSmetadata) the function getStyleableProperty tells something about <? capture of extends styleable
what is the type and how does it work.
I try to cast -fx-max-width to ( easyno subproperties) Styleable but does not work
hbox.getCSSMetadata().stream().filter(prop -> prop.toString().constanis("-fx-max-width")).forEach(prop2->{
prop2.getProperty(); //ok returns the string of the name
prop2.getStyleableProperty(<????? what goes here and what is the type of the returned value>);
});
Your prop2 variable (which is not really well named) is of type CSSMetaData<? extends Styleable, ?>.
The parameter you need to pass to the getStyleableProperty(...) method is the Styleable for which it's a property; since this CSSMetaData came from hbox, and I assume that's an HBox, then the parameter should be hbox.
However, the compiler will insist the parameter is the same type as the first type parameter to the CSSMetaData; since this is a wildcard type (? extends Styleable) there's no way for it to check this. So you need a downcast:
((CSSMetaData<HBox, ?>)prop2).getStyleableProperty(hbox)
The cast does not need to be this specific;
((CSSMetaData<Styleable, ?>)prop2).getStyleableProperty(hbox)
will also work (since hbox is an HBox, which is an implementation of Styleable).
Note that this is actually going to give you the hbox's maxWidthProperty(), so really you could just do hbox.maxWidthProperty() instead. (But maybe your -fx-max-width is just a contrived example, and you are trying to get this dynamically for some reason.)
Note that it's almost always bad practice to check an object's toString() method to determine data about it. So you should replace
prop.toString().contains("-fx-max-width")
with
prop.getProperty().equals("-fx-max-width)
The return type of getStyleableProperty(), with this downcast, will be
StyleableProperty<?>
(since there is a wildcard for the second type parameter in the downcast). If you knew more information, as in this case, you can make that more specific if needed. For example, if you wanted to set the value, you would need to use the fact that the -fx-max-width CSS property is numeric, and use the downcast
StyleableProperty<Number> maxWidth = ((CSSMetaData<Styleable, Number>)prop2).getStyleableProperty(hbox);
and then the return type would be
StyleableProperty<Number>
and you'd be able to do, for example
maxWidth.setValue(400.0);
Here's an example:
import javafx.application.Application;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class SPTest extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setAlignment(Pos.CENTER);
Button show = new Button("Show");
show.setOnAction(e -> {
root.getCssMetaData().stream().filter(cssMD -> cssMD.getProperty().equals("-fx-max-width")).forEach(maxWidthMD -> {
StyleableProperty<?> maxWidth = ((CssMetaData<Styleable, ?>)maxWidthMD).getStyleableProperty(root);
System.out.println(maxWidth.getValue());
System.out.println(maxWidth == root.maxWidthProperty());
});
System.out.println(root.getMaxWidth());
});
root.getChildren().add(show);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
I used a simple style.css file to test this:
.root {
-fx-max-width: 400 ;
}
Note the compiler has no way of checking the cast will work, so you get a compiler warning with this code. Since you're assured the cast will work (because the CSSMetaData was retrieved from the hbox), you can suppress this warning:
#SuppressWarnings("unchecked")
StyleableProperty<?> maxWidth = ((CssMetaData<HBox, ?>)prop2).getStyleableProperty(root);
I have spent the last few hours adding a block to my Minecraft Mod. I have looked at several tutorials and none of them work. The blocks are not added to the Creative Inventory and I can't set them by command either. Unfortunately I didn't have any bugs in the console that I could show here. At some point I gave up and tried to do armor, here the same problem. On the other hand: normal items work (You can see the Item "ruby" which woked finde).
Here the code of my main class:
package de.thom.clashOfClasses;
import de.thom.clashOfClasses.init.ArmorMaterialList;
import de.thom.clashOfClasses.init.BlockList;
import de.thom.clashOfClasses.init.ItemList;
import net.minecraft.block.Block;
import net.minecraft.block.SoundType;
import net.minecraft.block.material.Material;
import net.minecraft.inventory.EquipmentSlotType;
import net.minecraft.item.ArmorItem;
import net.minecraft.item.BlockItem;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroup;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
#Mod("clashofclasses")
public class ClashOfClasses {
public static ClashOfClasses instance;
public static final String modid = "clashofclasses";
public static final Logger logger = LogManager.getLogger(modid);
public ClashOfClasses() {
instance = this;
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientRegistries);
MinecraftForge.EVENT_BUS.register(this);
}
public void setup(final FMLCommonSetupEvent event) {
logger.info("Setup method complete");
}
public void clientRegistries(final FMLClientSetupEvent event) {
logger.info("ClientRegistries method complete");
}
#Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public static class RegistryEvents {
#SubscribeEvent
public static void registerItems(final RegistryEvent.Register<Item> event) {
logger.info("Item Registry started");
event.getRegistry().registerAll(
ItemList.RUBY,
ItemList.ruby_block = new BlockItem(BlockList.ruby_block,new Item.Properties().group(ItemGroup.MISC)).setRegistryName(BlockList.ruby_block.getRegistryName())
);
logger.info("Items registerd");
}
#SubscribeEvent
public static void registerBlocks(final RegistryEvent.Register<Block> event) {
logger.info("Block Registry started");
event.getRegistry().registerAll
(
BlockList.ruby_block = new Block(Block.Properties.create(Material.IRON).hardnessAndResistance(2.0f,3.0f).lightValue(5).sound(SoundType.METAL)).setRegistryName(location("ruby_block"))
);
logger.info("Blocks registerd");
}
private static ResourceLocation location(String name){
return new ResourceLocation(ClashOfClasses.modid, name);
}
}
}
Here is the code of BlockList
package de.thom.clashOfClasses.init;
import net.minecraft.block.Block;
public class BlockList {
public static Block ruby_block;
}
Here is the code of ItemList:
package de.thom.clashOfClasses.init;
import de.thom.clashOfClasses.ClashOfClasses;
import net.minecraft.inventory.EquipmentSlotType;
import net.minecraft.item.ArmorItem;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroup;
import net.minecraft.util.ResourceLocation;
public class ItemList
{
//Test Items
public static Item RUBY = new Item(new Item.Properties().group(ItemGroup.MATERIALS)).setRegistryName(location("ruby"));
public static Item ruby_block;
private static ResourceLocation location(String name){
return new ResourceLocation(ClashOfClasses.modid, name);
}
}
A block in the world and a “block” in an inventory are very different things. A block in the world is represented by an IBlockState, and its behavior defined by an instance of Block. Meanwhile, an item in an inventory is an ItemStack, controlled by an Item. As a bridge between the different worlds of Block and Item, there exists the class ItemBlock. ItemBlock is a subclass of Item that has a field block that holds a reference to the Block it represents. ItemBlock defines some of the behavior of a “block” as an item, like how a right click places the block. It’s possible to have a Block without an ItemBlock. (E.g. minecraft:water exists a block, but not an item. It is therefore impossible to hold it in an inventory as one.)
When a block is registered, only a block is registered. The block does not automatically have an ItemBlock. To create a basic ItemBlock for a block, one should use new ItemBlock(block).setRegistryName(block.getRegistryName()). The unlocalized name is the same as the block’s. Custom subclasses of ItemBlock may be used as well. Once an ItemBlock has been registered for a block, Item.getItemFromBlock can be used to retrieve it. Item.getItemFromBlock will return null if there is no ItemBlock for the Block, so if you are not certain that there is an ItemBlock for the Block you are using, check for null.
from https://mcforge.readthedocs.io/en/latest/blocks/blocks/.
I short, if everything works, your blocks shoudnt appear in your
#ObjectHolder(modid)
#Mod.EventBusSunscriber(modid = modid, bus = Bus.Mod)
public class BlockInit {
public static final Block example_block = null;
#SubscribeEvent
public static void registerBlocks(final RegistryEvent.Register<Block> event) {
event.getRegistry().register(new Block(Block.Properties.create(Material)).setRegistry("example_block"));
}
#SubscribeEvent
public static void registerBlockItems(final RegistryEvent.Register<Item> event){
event.getRegistry().register(new BlockItem(example_item, new Item.Properties().group(ItemGroup)).setRegistry("example_block"));
}
That works for me just replace example_block with the name of your block and add more properties if you want
for another block just repeat the event.getRegistry stuff and use the name of your new block instead of example_block.
and don't forget to do the json files
In my particular case I have a custom implementation of a TableCell that contains a Button. This button invokes a method that returns a String to be displayed instead of the button. The visual change is done by setting the graphic in the cell to null and setting the text to the String, using TableCell.setText(String).
What I've realized - and worked around so far, is that TableCell.setText(String) doesn't change the data value associated with the cell in the TableView. It just changes the visual representation of the cell. The underlying data structure is in my case a ObservableList<String> that represents a row, and each element in the list is, of course, cell data.
My current solution is to set the underlying value doing this:
getTableView().getItems().get(getIndex()).set(getTableView().getColumns().indexOf(getTableColumn()), "Value");
And this works fine. But I mean, the code is barely readable.
It seems like the data in the TableView and the TableCell are entirely separated, since you need to access the TableView to set the underlying data for a cell. There is a TableCell.getItem() to get the data value, but there's no setItem(String) method to set it.
I hope I explained my issue good enough.
Is there a better and prettier way to do this? Why doesn't just `TableCell.setText(String) change the data value as well?
Edit: I'll explain what I am trying to implement:
I basically have a table where one column contains a button that will load some arbitrary data to the column when pressed. Once the data has been loaded, the button is removed from the column and the data is displayed instead. That is basically it. This works fine unless the table is sorted/filtered. Here's a MCVE of my implementation:
import javafx.animation.Animation;
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.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
public class MCVE extends Application {
private final BooleanProperty countLoading = new SimpleBooleanProperty(this, "countLoading", false);
#Override
public void start(Stage stage) {
int numOfCols = 3;
ObservableList<ObservableList<String>> tableData = FXCollections.observableArrayList();
// Generate dummy data.
for (int i = 0; i < 100; i++) {
ObservableList<String> row = FXCollections.observableArrayList();
for (int j = 0; j < numOfCols; j++)
row.add("Row" + i + "Col" + j);
tableData.add(row);
}
TableView<ObservableList<String>> table = new TableView<ObservableList<String>>();
// Add columns to the table.
for (int i = 0; i < numOfCols; i++) {
if (i == 2) {
final int j = i;
table.getColumns().add(addColumn(i, "Column " + i, e -> new QueueCountCell(j, countLoading)));
} else {
table.getColumns().add(addColumn(i, "Column " + i, null));
}
}
table.getItems().addAll(tableData);
Scene scene = new Scene(table);
stage.setScene(scene);
stage.show();
}
/**
* Returns a simple column.
*/
private TableColumn<ObservableList<String>, String> addColumn(int index, String name,
Callback<TableColumn<ObservableList<String>, String>, TableCell<ObservableList<String>, String>> callback) {
TableColumn<ObservableList<String>, String> col = new TableColumn<ObservableList<String>, String>(name);
col.setCellValueFactory(e -> new SimpleStringProperty(e.getValue().get(index)));
if (callback != null) {
col.setCellFactory(callback);
}
return col;
}
public static void main(String[] args) {
launch();
}
class QueueCountCell extends TableCell<ObservableList<String>, String> {
private final Button loadButton = new Button("Load");
public QueueCountCell(int colIndex, BooleanProperty countLoading) {
countLoading.addListener((obs, oldValue, newValue) -> {
if (newValue) {
loadButton.setDisable(true);
} else {
if (getIndex() >= 0 && getIndex() < this.getTableView().getItems().size()) {
loadButton.setDisable(false);
}
}
});
final Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO, e -> setText("Loading .")),
new KeyFrame(Duration.millis(500), e -> setText("Loading . .")),
new KeyFrame(Duration.millis(1000), e -> setText("Loading . . .")),
new KeyFrame(Duration.millis(1500)));
timeline.setCycleCount(Animation.INDEFINITE);
loadButton.setOnAction(e -> {
new Thread(new Task<String>() {
#Override
public String call() throws InterruptedException {
// Simlute task working.
Thread.sleep(3000);
return "5";
}
#Override
public void running() {
setGraphic(null);
timeline.play();
countLoading.set(true);
}
#Override
public void succeeded() {
timeline.stop();
countLoading.set(false);
setText(getValue());
}
#Override
public void failed() {
timeline.stop();
countLoading.set(false);
setGraphic(loadButton);
setText(null);
this.getException().printStackTrace();
}
}).start();
});
}
#Override
public final void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setGraphic(loadButton);
}
}
}
}
Background: MVC
Much of JavaFX is designed around a Model-View-Controller (MVC) pattern. This is a loosely-defined pattern with many variants, but the basic idea is that there are three components:
Model: an object (or objects) that represent the data. The Model knows nothing about how the data is presented to the user.
View: an object that presents the data to the user. The view does not do any logical processing or store the data; it just knows how to convert the data to some kind of presentation for the user.
Controller: an object that modifies the data in the model, often (though not exclusively) in response to user input.
There are several variants of this pattern, including MVP, MVVM, supervising controller, passive view, and others, but the unifying theme in all of them is that there is a separation between the view, which simply presents data but does not otherwise "know" what the data is, and the model, which stores the state (data) but knows nothing about how it might be presented. The usually-cited motivation for this is the ability to have multiple views of the same data which have no need to refer to each other.
In the "classical" implementation of this, the view "observes" the model via some kind of subscriber-notification pattern (e.g. an observer pattern). So the view will register with the model to be notified of changes to the data, and will repaint accordingly. Often, since the controller relies on event listeners on the components in the view, the controller and view are tightly coupled; however there is always clear separation between the view and the model.
The best reference I know for learning more about this is Martin Fowler.
Background: JavaFX Virtualized Controls
JavaFX has a set of "virtualized controls", which includes ListView, TableView, TreeView, and TreeTableView. These controls are designed to be able to present large quantities of data to the user in an efficient manner. The key observation behind the design is that data is relatively inexpensive to store in memory, whereas the UI components (which typically have hundreds of properties) consume a relatively large amount of memory and are computationally expensive (e.g. to perform layout, apply style, etc). Moreover, in a table (for example) with a large amount of backing data, only a small proportion of those data are visible at any time, and there is no real need for UI controls for the remaining data.
Virtualized controls in JavaFX employ a cell rendering mechanism, in which "cells" are created only for the visible data. As the user scrolls around the table, the cells are reused to display data that was previously not visible. This allows the creation of a relatively small number of cells even for extremely large data sets: the number of (expensive) cells created is basically constant with respect to the size of the data. The Cell class defines an updateItem(...) method that is invoked when the cell is reused to present different data. All this is possible because the design is built on MVC principles: the cell is the view, and the data is stored in the model. The documentation for Cell has details on this.
Note that this means that you must not use the cell for any kind of data storage, because when the user scrolls in the control, that state will be lost. General MVC principles dictate that this is what you should do anyway.
The code you posted doesn't work correctly, as it violates these rules. In particular, if you click one of the "Load" buttons, and then scroll before the loading is complete, the cell that is performing the loading will now be referring to the wrong item in the model, and you end up with a corrupted view. The following series of screenshots occurred from pressing "Load", taking a screenshot, scrolling, waiting for the load to complete, and taking another screenshot. Note the value appears to have changed for an item that is different to the item for which "Load" was pressed.
To fix this, you have to have a model that stores all of the state of the application: you cannot store any state in the cells. It is a general truth in JavaFX that in order to make the UI code elegant, you should start with a well-defined data model. In particular, since your view (cell) changes when the data is in the process of loading, the "loading state" needs to be part of the model. So each item in each row in your table is represented by two pieces of data: the actual data value (strings in your case), and the "loading state" of the data.
So I would start with a class that represents that. You could just use a String for the data, or you could make it more general by making it a generic class. I'll do the latter. A good implementation will also keep the two states consistent: if the data is null and we have not explicitly stated it is loading, we consider it not loaded; if the data is non-null, we consider it loaded. So we have:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
public class LazyLoadingData<T> {
public enum LoadingState { NOT_LOADED, LOADING, LOADED }
private final ObjectProperty<T> data = new SimpleObjectProperty<>(null);
private final ReadOnlyObjectWrapper<LoadingState> loadingState
= new ReadOnlyObjectWrapper<>(LoadingState.NOT_LOADED);
public LazyLoadingData(T data) {
// listeners to keep properties consistent with each other:
this.data.addListener((obs, oldData, newData) -> {
if (newData == null) {
loadingState.set(LoadingState.NOT_LOADED);
} else {
loadingState.set(LoadingState.LOADED);
}
});
this.loadingState.addListener((obs, oldState, newState) -> {
if (newState != LoadingState.LOADED) {
this.data.set(null);
}
});
this.data.set(data);
}
public LazyLoadingData() {
this(null);
}
public void startLoading() {
loadingState.set(LoadingState.LOADING);
}
public final ObjectProperty<T> dataProperty() {
return this.data;
}
public final T getData() {
return this.dataProperty().get();
}
public final void setData(final T data) {
this.dataProperty().set(data);
}
public final ReadOnlyObjectProperty<LoadingState> loadingStateProperty() {
return this.loadingState.getReadOnlyProperty();
}
public final LazyLoadingData.LoadingState getLoadingState() {
return this.loadingStateProperty().get();
}
}
The model here will just be an ObservableList<List<LazyLoadingData<String>>>, so each cell is a LazyLoadingData<String> and each row is a list of them.
To make this properly MVC, let's have a separate controller class which has a way of updating data in the model:
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javafx.concurrent.Task;
public class LazyLoadingDataController {
// data model:
private final List<List<LazyLoadingData<String>>> data ;
private final Random rng = new Random();
private final Executor exec = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
public LazyLoadingDataController(List<List<LazyLoadingData<String>>> data) {
this.data = data ;
}
public void loadData(int column, int row) {
Task<String> loader = new Task<String>() {
#Override
protected String call() throws InterruptedException {
int value = rng.nextInt(1000);
Thread.sleep(3000);
return "Data: "+value;
}
};
data.get(row).get(column).startLoading();
loader.setOnSucceeded(e -> data.get(row).get(column).setData(loader.getValue()));
exec.execute(loader);
}
}
Now our cell implementation is pretty straightforward. The only tricky part is that each item has two properties, and we actually need to observe both of those properties and update the cell if either of them changes. We need to be careful to remove listener from items the cell is no longer displaying. So the cell looks like:
import java.util.List;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.util.Duration;
public class LazyLoadingDataCell<T>
extends TableCell<List<LazyLoadingData<T>>, LazyLoadingData<T>>{
private final Button loadButton = new Button("Load");
private final Timeline loadingAnimation = new Timeline(
new KeyFrame(Duration.ZERO, e -> setText("Loading")),
new KeyFrame(Duration.millis(500), e -> setText("Loading.")),
new KeyFrame(Duration.millis(1000), e -> setText("Loading..")),
new KeyFrame(Duration.millis(1500), e -> setText("Loading..."))
);
public LazyLoadingDataCell(LazyLoadingDataController controller, int columnIndex) {
loadingAnimation.setCycleCount(Animation.INDEFINITE);
loadButton.setOnAction(e -> controller.loadData(columnIndex, getIndex()));
// listener for observing either the dataProperty()
// or the loadingStateProperty() of the current item:
ChangeListener<Object> listener = (obs, oldState, newState) -> doUpdate();
// when the item changes, remove and add the listener:
itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.dataProperty().removeListener(listener);
oldItem.loadingStateProperty().removeListener(listener);
}
if (newItem != null) {
newItem.dataProperty().addListener(listener);
newItem.loadingStateProperty().addListener(listener);
}
doUpdate();
});
}
#Override
protected void updateItem(LazyLoadingData<T> item, boolean empty) {
super.updateItem(item, empty);
doUpdate();
}
private void doUpdate() {
if (isEmpty() || getItem() == null) {
setText(null);
setGraphic(null);
} else {
LazyLoadingData.LoadingState state = getItem().getLoadingState();
if (state == LazyLoadingData.LoadingState.NOT_LOADED) {
loadingAnimation.stop();
setText(null);
setGraphic(loadButton);
} else if (state == LazyLoadingData.LoadingState.LOADING) {
setGraphic(null);
loadingAnimation.play();
} else if (state == LazyLoadingData.LoadingState.LOADED) {
loadingAnimation.stop();
setGraphic(null);
setText(getItem().getData().toString());
}
}
}
}
Note how
The cell contains no state. The fields in the cell are entirely related to the display of data (a button and an animation).
The action of the button doesn't (directly) change anything in the view. It simply tells the controller to update the data in the model. Because the cell (view) is observing the model, when the model changes, the view updates.
The model also changes independently of user action, when the task in the controller completes. Because the view is observing the model for changes, it updates automatically.
Finally an example using this. There is not much unexpected here, we just create a model (ObservableList of List<LazyLoadingData<String>>), create a controller, and then a table with some columns.
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class LazyLoadingTableExample extends Application {
private final int numCols = 3 ;
private final int numRows = 100 ;
#Override
public void start(Stage primaryStage) {
TableView<List<LazyLoadingData<String>>> table = new TableView<>();
// data model:
ObservableList<List<LazyLoadingData<String>>> data
= FXCollections.observableArrayList();
table.setItems(data);
LazyLoadingDataController controller = new LazyLoadingDataController(data);
// build data:
for (int i = 0; i < numRows; i++) {
ObservableList<LazyLoadingData<String>> row
= FXCollections.observableArrayList();
for (int j = 0 ; j < numCols - 1 ; j++) {
row.add(new LazyLoadingData<>("Cell ["+j+", "+i+"]"));
}
row.add(new LazyLoadingData<>());
data.add(row);
}
for (int i = 0 ; i < numCols ; i++) {
table.getColumns().add(createColumn(controller, i));
}
Scene scene = new Scene(table, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<List<LazyLoadingData<String>>,LazyLoadingData<String>>
createColumn(LazyLoadingDataController controller, int columnIndex) {
TableColumn<List<LazyLoadingData<String>>,LazyLoadingData<String>> col
= new TableColumn<>("Column "+columnIndex);
col.setCellValueFactory(cellData ->
new SimpleObjectProperty<>(cellData.getValue().get(columnIndex)));
col.setCellFactory(tc ->
new LazyLoadingDataCell<>(controller, columnIndex));
return col ;
}
public static void main(String[] args) {
launch(args);
}
}
Hi I am trying to read a the numbers from a text field that shows a price e.g. £3.00, and convert the value of the price to a double. Is there a way to do
Double value;
value = Double.parseDouble(textField.getText());
But it won't let me do that because of the £ sign. Is there a way to strip the pound sign then read the digits.
Thanks
There is some TextFormatter and change filter handling logic built into the JavaFX TextField API, you could make use of that.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.text.ParseException;
class CurrencyFormatter extends TextFormatter<Double> {
private static final double DEFAULT_VALUE = 5.00d;
private static final String CURRENCY_SYMBOL = "\u00A3"; // british pound
private static final DecimalFormat strictZeroDecimalFormat
= new DecimalFormat(CURRENCY_SYMBOL + "###,##0.00");
CurrencyFormatter() {
super(
// string converter converts between a string and a value property.
new StringConverter<Double>() {
#Override
public String toString(Double value) {
return strictZeroDecimalFormat.format(value);
}
#Override
public Double fromString(String string) {
try {
return strictZeroDecimalFormat.parse(string).doubleValue();
} catch (ParseException e) {
return Double.NaN;
}
}
},
DEFAULT_VALUE,
// change filter rejects text input if it cannot be parsed.
change -> {
try {
strictZeroDecimalFormat.parse(change.getControlNewText());
return change;
} catch (ParseException e) {
return null;
}
}
);
}
}
public class FormattedTextField extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(final Stage stage) {
TextField textField = new TextField();
textField.setTextFormatter(new CurrencyFormatter());
Label text = new Label();
text.textProperty().bind(
Bindings.concat(
"Text: ",
textField.textProperty()
)
);
Label value = new Label();
value.textProperty().bind(
Bindings.concat(
"Value: ",
textField.getTextFormatter().valueProperty().asString()
)
);
VBox layout = new VBox(
10,
textField,
text,
value,
new Button("Apply")
);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
}
}
The exact rules for DecimalFormat and the filter could get a little tricky if you are very particular about user experience (e.g. can the user enter the currency symbol? what happens if the user does not enter a currency symbol? are empty values permitted? etc.) The above example offers a compromise between a reasonable user experience and a (relatively) easy to program solution. For an actual production level application, you might wish to tweak the logic and behavior a bit more to fit your particular application.
Note, the apply button doesn't actually need to do anything to apply the change. Changes are applied when the user changes focus away from the text field (as long as they pass the change filter). So if the user clicks on the apply button, it gains, focus, the text field loses focus and the change is applied if applicable.
The above example treats the currency values as doubles (to match with the question), but those serious about currency may wish to look to BigDecimal.
For a simpler solution using similar concepts, see also:
Java 8 U40 TextFormatter (JavaFX) to restrict user input only for decimal number
I'm looking to add a separator into a choice box and still retain the type safety.
On all of the examples I've seen, they just do the following:
ChoiceBox<Object> cb = new ChoiceBox<>();
cb.getItems().addAll("one", "two", new Separator(), "fadfadfasd", "afdafdsfas");
Has anyone come up with a solution to be able to add separators and still retain type safety?
I would expect that if I wanted to add separators, I should be able do something along the following:
ChoiceBox<T> cb = new ChoiceBox<T>();
cb.getSeparators().add(1, new Separator()); // 1 is the index of where the separator should be
I shouldn't have to sacrifice type safety just to add separators.
As already noted, are Separators only supported if added to the items (dirty, dirty). To support them along the lines expected in the question, we need to:
add the notion of list of separator to choiceBox
make its skin aware of that list
While the former is not a big deal, the latter requires a complete re-write (mostly c&p) of its skin, as everything is tightly hidden in privacy. If the re-write has happened anyway, then it's just a couple of lines more :-)
Just for fun, I'm experimenting with ChoiceBoxX that solves some nasty bugs in its selection handling, so couldn't resist to try.
First, add support to the ChoiceBoxx itself:
/**
* Adds a separator index to the list. The separator is inserted
* after the item with the same index. Client code
* must keep this list in sync with the data.
*
* #param separator
*/
public final void addSeparator(int separator) {
if (separatorsList.getValue() == null) {
separatorsList.setValue(FXCollections.observableArrayList());
}
separatorsList.getValue().add(separator);
};
Then some changes in ChoiceBoxXSkin
must listen to the separatorsList
must expect index-of-menuItem != index-of-choiceItem
menuItem must keep its index-of-choiceItem
At its simplest, the listener re-builds the popup, the menuItem stores the dataIndex in its properties and all code that needs to access a popup by its dataIndex is delegated to a method that loops through the menuItems until it finds one that fits:
protected RadioMenuItem getMenuItemFor(int dataIndex) {
if (dataIndex < 0) return null;
int loopIndex = dataIndex;
while (loopIndex < popup.getItems().size()) {
MenuItem item = popup.getItems().get(loopIndex);
ObservableMap<Object, Object> properties = item.getProperties();
Object object = properties.get("data-index");
if ((object instanceof Integer) && dataIndex == (Integer) object) {
return item instanceof RadioMenuItem ? (RadioMenuItem)item : null;
}
loopIndex++;
}
return null;
}
Well you can work around it by creating an interface and then subclassing Separator to implement this interface:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class ChoiceBoxIsSafe extends Application {
interface FruitInterface { }
static public class Fruit implements FruitInterface {
private StringProperty name = new SimpleStringProperty();
Fruit(String name) {
this.name.set(name);
}
public StringProperty nameProperty() {
return name;
}
#Override
public String toString() {
return name.get();
}
}
static public class FruitySeparator extends Separator implements FruitInterface { }
#Override
public void start(Stage primaryStage) throws Exception {
GridPane grid = new GridPane();
grid.setHgap(10); grid.setVgap(10); grid.setPadding(new Insets(10));
ChoiceBox<FruitInterface> cb = new ChoiceBox<>();
cb.getItems().addAll(new Fruit("Apple"), new Fruit("Orange"), new FruitySeparator(), new Fruit("Peach"));
Text text = new Text("");
ReadOnlyObjectProperty<FruitInterface> selected = cb.getSelectionModel().selectedItemProperty();
text.textProperty().bind(Bindings.select(selected, "name"));
grid.add(cb, 0, 0);
grid.add(text, 1, 0);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
but that is hardly an "elegant" solution and cannot be done in all cases (e.g. ChoiceBox<String>).
From the implementation of ChoiceBox it certainly looks like it wasn't a good idea to treat Separators like items in the ChoiceBox :-(.
FOR THE REST OF US:
There is a MUCH easier way to do this using code (there are easy ways to do it using FXML too, doing it in code offers more flexibility).
You simply create an ObservableList, then populate it with your items, including the separator then assign that list to the ChoiceBox like this:
private void fillChoiceBox(ChoiceBox choiceBox) {
ObservableList items = FXCollections.observableArrayList();
items.add("one");
items.add("two");
items.add("three");
items.add(new Separator());
items.add("Apples");
items.add("Oranges");
items.add("Pears");
choiceBox.getItems().clear();
choiceBox.getItems().addAll(items);
}