ControlsFX:确保 PopOver 箭头始终指向正确的位置

ControlsFX: Ensure PopOver arrow always points to the right spot

我在 TableView 中使用 ControlsFX 中的 PopOver 如果我触发单元格的 startEdit,它应该弹出 PopOver。这部分有效,问题是,指向该行的箭头每次都不在正确的位置。如果我 select 来自 table 的一行,它位于 table 的底部,它指向它上面的一个单元格。

我需要那个箭头每次都指向 TableView 中的正确单元格。

ControlsFX , 版本: 8.40.14

我该如何解决这个问题?

这是您可以看到其工作原理的代码:

package Whosebug.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();
        }
    }

}

这里有两个截图来说明我的意思:

这更糟糕:(setAutoFix(false)

尝试在 PopOver 实例上设置 setAutoFix(false)。来自 PopOver 的超类 PopupWindow 的 autoFix 属性 的文档:

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.

我不是 ControlFX 专家,但我相信您遇到的问题是因为 PopOver 的高度大于当前屏幕尺寸,因此它试图以某种方式将自身重新定位到屏幕局部边界内.因此,为了实现您正在尝试的目标,您需要手动设置 PopOver 控件的 ArrowLocation。这是解决问题的方法(使用您的代码):

    @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);
    }

使用上面的代码,您会看到一些需要更改的地方,并且需要更仔细地考虑。

  • 第一个是您需要为您的 StackPane 设置一个特定的大小或找到一个动态的方法来计算它。

  • 其次,在我的示例中,我使用的是 Screen.getPrimary(),它将获取 primary 屏幕的 Rectangle2D 尺寸,而不是您拥有的屏幕应用程序,这意味着如果您有更多不同分辨率的显示器并且您的程序显示在第二个显示器上,上面的代码仍将使用第一个(默认)显示器的分辨率,这可能与主要显示器不匹配,因此您将不得不找到一种方法来获得正确的显示器分辨率。

  • 最后,当 window 位于屏幕右侧时,您需要执行相同的操作,因为 'Popover' 的宽度将超过您的宽度监控