I am using PopOver
from ControlsFX, in a TableView
If I trigger the startEdit
of a cell, it should pop the PopOver
. This part it works, the problem is, the arrow which is pointing to the row is not on the right place every time. If I select a row from the table which is at the bottom of the table , it points to a cell above it.
I need that arrow to point every time to the right cell in the TableView
.
ControlsFX , version: 8.40.14
How can I solve this?
Here is the code where you can see how it works:
package stackoverflow.popover;
import com.sun.deploy.util.StringUtils;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.controlsfx.control.PopOver;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
public class Controller implements Initializable {
@FXML
private TableView<Model> table;
@FXML
private TableColumn<Model, ObservableList<String>> listCell;
@Override
public void initialize(URL location, ResourceBundle resources) {
Model model = new Model(FXCollections.observableArrayList("Apple", "Peach"));
ObservableList<Model> items = FXCollections.observableArrayList();
for (int i = 0; i < 50; i++) {
items.add(model);
}
table.setItems(items);
table.setEditable(true);
listCell.setCellFactory(factory -> new ListTableCell(
FXCollections.observableArrayList("Apple", "Orange", "Peach", "Banana", "Lemon", "Lime")));
listCell.setCellValueFactory(data -> data.getValue().list);
}
private class ListTableCell extends TableCell<Model, ObservableList<String>> {
private ObservableList<String> allItems;
ListTableCell(ObservableList<String> allItems) {
this.allItems = allItems;
}
@Override
public void startEdit() {
super.startEdit();
PopOver popOver = new PopOver();
popOver.setAutoHide(true);
PopupController sc = new PopupController(allItems, new ArrayList<>(getItem()));
popOver.setContentNode(new StackPane(sc.getPane()));
popOver.setOnHiding(event -> commitEdit(sc.getItems()));
popOver.show(this);
}
@Override
protected void updateItem(ObservableList<String> item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(StringUtils.join(item, ","));
}
}
}
private class Model {
ListProperty<String> list;
public Model(ObservableList<String> list) {
this.list = new SimpleListProperty<>(list);
}
}
private class PopupController {
private BorderPane pane = new BorderPane();
private ListView<String> left = new ListView<>();
private ListView<String> right = new ListView<>();
private Button toLeft = new Button("<");
private Button toRight = new Button(">");
PopupController(List<String> all, List<String> selected) {
VBox leftBox = new VBox();
leftBox.setSpacing(5);
leftBox.getChildren().add(toRight);
leftBox.getChildren().add(left);
pane.setLeft(leftBox);
VBox rightBox = new VBox();
rightBox.setSpacing(5);
rightBox.getChildren().add(toLeft);
rightBox.getChildren().add(right);
pane.setRight(rightBox);
ObservableList<String> allItems = FXCollections.observableArrayList(all);
allItems.removeAll(selected);
left.setItems(allItems);
right.setItems(FXCollections.observableArrayList(selected));
toLeft.disableProperty().bind(right.getSelectionModel().selectedItemProperty().isNull());
toRight.disableProperty().bind(left.getSelectionModel().selectedItemProperty().isNull());
toLeft.setOnAction(event -> {
String str = right.getSelectionModel().getSelectedItem();
right.getItems().remove(str);
left.getItems().add(str);
});
toRight.setOnAction(event -> {
String str = left.getSelectionModel().getSelectedItem();
left.getItems().remove(str);
right.getItems().add(str);
});
}
BorderPane getPane() {
return pane;
}
ObservableList<String> getItems() {
return right.getItems();
}
}
}
Here are two screenshots to show what I mean :
I am not expert with ControlFX but I believe the problem you are facing its because the height of your PopOver is greater than your current screen size thus it is trying to relocate itself in a way to be inside the screen local bounds. So in order to achieve what you are trying you will need to manually set the ArrowLocation of your PopOver control. Here is how you can solve the issue (using your code) :
@Override
public void startEdit() {
super.startEdit();
PopOver popOver = new PopOver();
popOver.setAutoHide(true);
// first set auto fix to false
// to manually set the arrow location
popOver.setAutoFix(false);
PopupController sc = new PopupController(allItems, new ArrayList<>(getItem()));
// set a specific height for our pane
final double paneHeight = 300;
StackPane popOverPane = new StackPane(sc.getPane());
popOverPane.setPrefHeight(paneHeight);
popOver.setContentNode(popOverPane);
popOver.setOnHiding(event -> commitEdit(sc.getItems()));
// find coordinates relative to the screen
Bounds screenBounds = this.localToScreen(this.getBoundsInLocal());
// get our current y position ( on screen )
int yPos = (int) screenBounds.getMinY();
// get screen size
Rectangle2D primaryScreenBounds = Screen.getPrimary().getVisualBounds();
int screenHeight = (int) primaryScreenBounds.getHeight();
// if the PopOver height + the current position is greater than
// the max screen's height then set the arrow position to bottom left
if(screenHeight < yPos + paneHeight) {
popOver.setArrowLocation(ArrowLocation.LEFT_BOTTOM);
}
popOver.show(this);
}
Using the code above you would see some things you need to change and think more carefully.
The first one is that you will need to set a specific size for your StackPane or to find a dynamic way to calculate it.
Secondly in my example I am using the Screen.getPrimary()
which will get the Rectangle2D dimensions of your primary screen and not the screen you have your application, this means that if you have more monitors with different resolution and your program is displayed on the second one, the code above will still use the first ( default ) monitor's resolution which might not match with the primary one, so you will have to find a way to get the correct monitor resolution.
Lastly you will need to do the same when the window is on the right side of the screen because then the width of the 'Popover' will exceed the width of your monitor