3

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

Sunflame
  • 2,730
  • 3
  • 16
  • 38

2 Answers2

2

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

JKostikiadis
  • 2,589
  • 2
  • 19
  • 31
  • 1
    Sorry I couldn't react to your answer, but it worked almost as I expected, but I got the Idea so I could go further with it. I changed it to show the arrow in the middle if it was opened in the middle of the table, and show the arrow on the right side if the column is on the right of the screen so the popup couldn't point to the cell where it was opened.The multiple screen usage is not a problem for me, but thanks that you mentioned it. Thank you very much for the answer ;) – Sunflame Nov 09 '18 at 10:39
-1

Try setting setAutoFix(false) on the PopOver instance. From the documentation of the autoFix property of the PopOver's superclass PopupWindow:

This convenience variable indicates whether, when the popup is shown, it should automatically correct its position such that it doesn't end up positioned off the screen.

  • This won't fix the problem, I don't want to have a window out of my screen I simply want to being displayed on the screen with the correct arrow position. – Sunflame Oct 11 '18 at 12:52
  • @Sunflame but what _do_ you want if the position doesn't fit? it has to go somewhere: either leave the screen or pop to the top or .. ? – kleopatra Nov 01 '18 at 09:19
  • As you can see on the screenshots I want that "Arrow" to always piont to the selected row. If I use this solution, the window goes off-screen which is even worst. – Sunflame Nov 01 '18 at 09:47
  • I have added a screenshot, how it looks like if I set `autoFix` to `false` – Sunflame Nov 01 '18 at 09:53
  • 1
    ahh .. I think I understand now: you want a) position the popup on the screen such as it fits and b) move the arrow along the side of the popup such that it points to the tableRow? You probably have to implement it yourself .. ideally, do it then contribute back into controlsfx :) – kleopatra Nov 01 '18 at 13:59