ControlsFX: Ensure PopOver arrow always points to the right spot

Sunflame :

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 :

enter image description here enter image description here This is even worst: (with setAutoFix(false)) enter image description here

JKostikiadis :

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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=109891&siteId=1