Getting id from a ActionEvent in JavaFX - javafx

I have a group of ToggleButtons that need to find out their id. Currently I can check the id in the getSource() and toString() method like this:
#FXML
public void btnCell(ActionEvent actionEvent) {
System.out.println(actionEvent.getSource());
}
Prints: ToggleButton[id=btn00, styleClass=toggle-button]''
Can I extract the id without relying on some shady substring busniess on that string?

hope it helps:
import javafx.scene.Node;
....
#FXML
public void btnCell(ActionEvent actionEvent) {
final Node source = (Node) actionEvent.getSource();
String id = source.getId();
Scene scene = source.getScene();
scene.lookup("#"+id).getStyleClass() ;
}

In case this is still relevant, I use a whole bunch of programmatically generated buttons (representing menu items in a POS application), identified through MyButton.setUserData(MyProdID), which is loaded from the product IDs in a database table. Then you can get that with MyButton.getUserData() in the ActionEvent handler.

Related

JavaFX: ComboBox with custom cell factory - buggy rendering

I have a ComboBox with a custom cell factory, but the rendering is buggy and I don't know what I'm doing wrong.
The ComboBox's data is based on an enum, let's call it MyEnum. So my ComboBox is defined as:
#FXML
private ComboBox<MyEnum> comboBox;
I am setting it up like this:
comboBox.getItems().addAll(MyEnum.values());
comboBox.setButtonCell(new CustomRenderer());
comboBox.setCellFactory(cell -> new CustomRenderer());
comboBox.getSelectionModel().select(0);
My CustomRenderer looks like this:
public class CustomRenderer extends ListCell<MyEnum> {
#Override
protected void updateItem(MyEnum enumValue, boolean empty) {
super.updateItem(enumValue, empty);
if (enumValue== null || empty) {
setGraphic(null);
setText(null);
} else {
setGraphic(enumValue.getGraphic());
}
}
}
The getGraphic() method of MyEnum returns a HBox which can contain any number of elements, but in my specific case it's just an ImageView:
public enum MyEnum{
ONE(new ImageView(new Image(MyApp.class.getResourceAsStream("image1.png"),
15,
15,
true,
true))),
TWO(new ImageView(new Image(MyApp.class.getResourceAsStream("image2.png"),
15,
15,
true,
true)));
private final HBox graphic;
MyEnum(Node... graphicContents) {
graphic = new HBox(graphicContents);
graphic.setSpacing(5);
}
public HBox getGraphic() {
return graphic;
}
}
Now, when I start the app, the first bug is that the ComboBox doesn't show anything selected, even though I have the comboBox.getSelectionModel().select(0) in the initialization:
When I click on it, the dropdown is correct and shows my two entries with their images:
When I select one of the entries, everything still seems fine:
But when I open the dropdown again, then it looks like this:
So suddenly the selected image is gone from the dropdown.
After I select the other entry where the icon is still displayed, and reopen the dropdown, then both images are gone. The images are still shown in the ButtonCell though, just not in the dropdown.
I first thought maybe it has something to do specifically with ImageViews, but when I replaced them with other nodes, like Labels, it was still the same 2 bugs:
Nothing shown as selected on app start
Everything that I click in the dropdown box is then gone from the dropdown
If a runnable sample is needed, let me know. But maybe someone can already spot my mistake from the given code.
Thx
Your issue is that a node cannot appear in the scene graph more than once.
From the node documentation:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in all of the following: as the root node of a Scene, the children ObservableList of a Parent, or as the clip of a Node.
If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent.
You are trying to reuse the same node in both the button for the list selection and in the selection list itself, which is not allowed.
Additionally, as noted in comments by kleopatra: "it's wrong to use nodes as data". One reason for that (among others), is that if you want to have more than one view of the same data visible at the same time, you won't be able to because the node related to the data can only be attached to the scene at a single place at any given time.
I'd especially recommend not placing nodes in enums. In my opinion, that it is really not what enums are for.
What you need to do is separate the model from the view. The cell factory is creating a view of the changing value of the enum which is the model. This view only needs to be created once for the cell and it needs to be updated (by providing the view with the appropriate image) whenever the cell value changes.
Example Code
You can see in the example that, because we have created two separate views of the same Monster (the dragon), the example is able to render both views at the same time. This is because the views are completely different nodes, that provide a view to the same underlying data (an enum value and its associated image).
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class ChooseYourDoomApp extends Application {
public static final String CSS = "data:text/css," + // language=CSS
"""
.root {
-fx-background-color: lightblue;
-fx-base: palegreen;
}
""";
#Override
public void start(Stage stage) throws Exception {
ComboBox<Monster> choiceOfDoom = new ComboBox<>(
FXCollections.observableArrayList(
Monster.values()
)
);
choiceOfDoom.setButtonCell(new MonsterCell());
choiceOfDoom.setCellFactory(listView -> new MonsterCell());
choiceOfDoom.getSelectionModel().select(Monster.Dragon);
StackPane layout = new StackPane(choiceOfDoom);
layout.setPadding(new Insets(20));
Scene scene = new Scene(layout);
scene.getStylesheets().add(CSS);
stage.setScene(scene);
stage.show();
}
public class MonsterCell extends ListCell<Monster> {
private ImageView imageView = new ImageView();
#Override
protected void updateItem(Monster item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
imageView.setImage(null);
setGraphic(null);
} else {
imageView.setImage(monsterImages.get(item));
setGraphic(imageView);
}
}
}
public enum Monster {
Medusa,
Dragon,
Treant,
Unicorn
}
private Map<Monster, Image> monsterImages = createMonsterImages();
private Map<Monster, Image> createMonsterImages() {
Map<Monster, Image> monsterImages = new HashMap<>();
for (Monster monster : Monster.values()) {
monsterImages.put(
monster,
new Image(
Objects.requireNonNull(
ChooseYourDoomApp.class.getResource(
monster + "-icon.png"
)
).toExternalForm()
)
);
}
return monsterImages;
}
public static void main(String[] args) {
launch(args);
}
}
Icons are placed in the resource directory under the same hierarchy as the package containing the main application code.
https://iconarchive.com/show/role-playing-icons-by-chanut.html
Dragon-icon.png
Medusa-icon.png
Treant-icon.png
Unicorn-icon.png

Selecting multiple objects and displaying selection in JavaFX TableView

I am trying to display a scene with a TableView where a user can click to select teams, which are then stored in an ObservableList. The selected teams should be highlighted and clicking a selected team should unselect them. Selecting multiple teams should not require holding down ctrl etc. The problem that I'm having is actually displaying the selected teams. I'm very confused with JavaFX's SelectionModel and FocusModel. I'd also need a functionality where if the user opens the view again to make changes to the selection, the selected teams should appear highlighted again.
Here's a sample of the code I've been working with. So far the selection works but the UI doesn't reflect the actual selection. In the image [1] is shown what I'm trying to achieve, the selection has been done holding ctrl and clicking the items. Again that doesn't reflect the actual selection and having to hold down keys to select multiple items is not viable in this case.
//for the sake of simplicity the Team objects are set here as a placeholder instead of fetching from the database
private Team[] teams = new Team[] {new Team("team1", 0), new Team("team2", 1), new Team("team3", 2), new Team("team4", 3)
private ObservableList<Team> selectedTeams = FXCollections.observableArrayList();
#FXML
private TableView<Team> teamsTableView;
#FXML
private TableColumn<Team, String> teamNameColumn;
#FXML
private TableColumn<Team, Integer> teamIdColumn;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
teamNameColumn.setCellValueFactory(new PropertyValueFactory<Team, String>("teamName"));
teamIdColumn.setCellValueFactory(new PropertyValueFactory<Team, Integer>("id"));
teamsTableView.setItems(teams);
teamsTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
teamsTableView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Team>() {
#Override
public void changed(ObservableValue<? extends Team> observable, Team oldValue, Team newValue) {
if(selectedTeams.contains(newValue)) {
selectedTeams.remove(newValue);
} else {
selectedTeams.add(newValue);
}
}
});
}
What approach should I try to take? Could the SelectionModel reference an ObservableList somehow?
I've been working on this for over two weeks now and I'm completely stumped. This is clearly beyond my abilities and some hand-holding might be necessary.

Link two Spinner with a button

I want the user to do this: when they select an item from the first spinner and the item on the other spinner and click on the button they get the result in a TextView. How can I do that?
I created already two spinner , TextView and one button. For example: if the user select from the first spinner their name and the second spinner their id, when they click to the button the text view will show their telephone number .
UPDATE:
Here is the code for the MainActivity
package second.program.p1;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.AdapterView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
Spinner v1;
Spinner v2;
ArrayAdapter<CharSequence> adapter;
TextView T1;
Button b;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
v1 = (Spinner) findViewById(R.id.v1);
adapter = ArrayAdapter.createFromResource(this,R.array.city_names,android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
v1.setAdapter(adapter);
v1.getSelectedItemPosition();
v2 = (Spinner) findViewById(R.id.v2);
adapter = ArrayAdapter.createFromResource(this,R.array.city_names,android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
v2.setAdapter(adapter);
T1 = (TextView) findViewById((R.id.T1));
b = (Button) findViewById(R.id.b);
}
};
Strings.xml
<resources>
<string name="app_name">P1</string>
<string-array name="city_names">
<item>" "</item>
<item>"Med"</item>
<item>"Ryu"</item>
<item>"Jedd"</item>
<item>"Hai"</item>
</string-array>
</resources>
So , i want after the user select "Med" from the first spinner (v1) and "Hai" from the second spinner (v2) he press the Button ,after that in the TextView will appers the km (the distance between the two cites, but i dont add it yet)
i did the two spinners ,
now what i should do with the TextView and the Button ?
As you have updated the question, so i can now answer it properly,
There are two methods that are provided by Android which we need to override to handle the items selection of spinners.
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// On selecting a spinner item
String cityName = parent.getItemAtPosition(position).toString();
// Showing selected spinner item
Toast.makeText(MainActivity, "Selected city: " + cityName, Toast.LENGTH_LONG).show();
}
public void onNothingSelected(AdapterView<?> view) {
// This method helps when we have to handle when user hasn't selected anything
from spinner on which further events are dependent.
}
Now do button click on your Button b like this in your onCreate:
b.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View view){
String firstCityName = v1.getSelectedItem().toString();
String secondCityName = v2.getSelectedItem().toString();
//now you have both items/cities you selected from spinners
// so its time to do some checks and show user the distance
if(firstCityName.equals("Meh") && secondCityName.equals("Med"){
T1.setText("the value which you have stored somewhere");
} else if(firstCityName.equals("Med") && secondCityName.equals("Ryu"){
T1.setText("30KM / hardcode it or get the value you stored somewhere");
} //.... if-else-if for remaining cases
}
}
Some advices :
1- Please use proper naming conventions.
Spinner spinnerStartingCity = new .....
Button btnSubmit = new ....
2- Topics like these are not so hard to find over internet, first search properly, because what almost all coders DO ?? GOOGLE -> copy/paste !! :D
3- Before googling, if you want to be a Developer not just coder, first go through proper tutorials, android dev site, and online tutorials for thorough understanding because learning lasts longer than copy pasting :)
Happy Developing

Dynamically Adding context menu items to tableview columns

I have the following controller that is instantiated many times on my gui. The reason is beacause it has a tableview that gets filled with different kind of data. Looks like this
class Controller {
#FXML
TableView<Map<String, String> myTable;
private Manager manager;
//Each TableView has different ammount of columns with different names that get dynamically added to the table view using this function
public void setUpColumns(List<TableColumn<Map<String, String>, String>> columns){
myTable.getColumns().addAll(columns);
addContextMenuToColumnHeaders();
}
private addContextMenuToColumnHeaders(){
for (TableColumn<Map<String, String>, ?> tc : myTable.getColumns()){
ContextMenu addToGraphContextMenu = createAddToGraphContextMenu(tc);
tc.setContextMenu(addToGraphContextMenu);
}
}
private ContextMenu createAddToGraphContextMenu(TableColumn<Map<String, String> String> tc){
for (MangerHandledObject mHO : manager.getHandledObjects()){
MenuItem menuItem = new MenuItem(mHO.getName());
menuItem.setOnAction(new EventHandler<ActionEvent>(){
#Override
public void handle(ActionEvent event){
//I want each menu item to have access to the column that is added to get the name of the column. Even after dynamically adding new menuItems
manager.callMethod(tc.getName());
}
});
}
}
}
The manager handled objects are not static. So the are added and deleted from the list that the manager keeps. I tried this
contextMenu.setOnShowing(......)
and before showing it will always check for the list from the manager and re-make the context menu items. But the problems is that when this executes I don't have access to the columns anymore. Is there any way to bypass this? Should I implement my own context menu to have a field of the column Name?
It worked. But I had to add at least one dummy MenuItem on my context menu in order for it to appear.

ObservableList to TableView javafx

I'm programming with a library that I don't know the code, only the methods, and I can't modify it. I tried to make a tableview of "flights" and it works but I don't know how to put a name(or ID) for each fly. Can someone help me? Thanks. Some code here:
public class StageController {
#FXML private TableView<Flight> flightsTable;
#FXML private TableColumn<Flight, String> flightColumn;
public void start(Airport air){
final AirportFlights a = Data.getInstance().getAirportFlights(air);
ObservableList<Flight> flights = FXCollections.observableArrayList(a.getArrivals().getFlights().values());
flightsTable.setItems(flights);
}
You need to declare a valueFactory for your tablecolumn. If you have a field name inside your Flight class then, you can do :
flightColumn.setCellValueFactory(
new PropertyValueFactory<Flight, String>("name"));

Resources