Search code examples
javafxpopovertabpanel

Using a JavaFX PopOver for a TabPanes Tab to peek at its content


I try to create an Peek Control for a TabPane. So if an Panel was already opened an Snapshot gets saved. When the user hovers over the Tab it will show a PopOver. This is similar to Windows Aero Peek.

My problem is that the PopOver gets hidden when it should not... because it receives a hide() event from the Tabs Graphic.

I created a small runnable Example to demonstrate the Problem:

import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import org.controlsfx.control.PopOver;
import org.controlsfx.control.PopOver.ArrowLocation;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.control.SingleSelectionModel;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class Main extends Application {
    Peek peek;
    Tab lastPeek;
    boolean show = false;

    public static void main(String[] args) {
        System.out.println("WE HAVE PROBLEMS WITH FOCUS HERE");
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        peek = new Peek();
        primaryStage.setTitle("Tabs");
        Group root = new Group();
        Scene scene = new Scene(root, 400, 250, Color.WHITE);

        TabPane tabPane = new TabPane();

        BorderPane borderPane = new BorderPane();

        final PopOver popOver = new PopOver();
        popOver.setArrowLocation(ArrowLocation.TOP_LEFT);
        final ImageView preview = new ImageView();
        preview.setFitHeight(100);
        preview.setSmooth(true);
        preview.setPreserveRatio(true);
        popOver.setContentNode(preview);

        for (int i = 0; i < 5; i++) {
            Tab tab = new Tab();
            HBox hbox = new HBox();
            hbox.getChildren().add(new Label("", new ImageView(new Image(Main.class.getResourceAsStream(i + ".jpg")))));
            hbox.setAlignment(Pos.CENTER);
            tab.setContent(hbox);
            tabPane.getTabs().add(tab);

            final Label decoration = new Label("Tab" + i);
            tab.setGraphic(decoration);
            System.out.println("tab.getGraphic() = " + tab.getGraphic());

            final int index = i;
            tab.getGraphic().setOnMouseEntered(mouseEvent -> {
                boolean show = false;
                if (lastPeek != tab) {
                    // Show PopOver when mouse enters label
                    Image previewImage = peek.get(tab);
                    if (previewImage != null) {
                        show = true;
                    }
                    System.out.println("previewImage = " + previewImage);
                    preview.setImage(previewImage);
                    System.out.println("show() " + "Tab" + index);
                    lastPeek = tab;
                }
                if (!tab.isSelected() && !tab.isDisabled() && show) {
                    popOver.show((Node) mouseEvent.getSource(), -3);
                }
            });

            tab.getGraphic().setOnMouseExited(mouseEvent -> {
                // Hide PopOver when mouse exits label
                // how can we handle this correct??
                 popOver.hide();
                System.out.println("hide() " + "Tab" + index);
            });

        }

        tabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() {
            @Override
            public void changed(ObservableValue<? extends Tab> ov, Tab oldTab, Tab newTab) {
                Image preview = oldTab.getContent().snapshot(new SnapshotParameters(), null);
                peek.set(oldTab, preview);
            }
        });
        tabPane.selectionModelProperty().addListener(new ChangeListener<SingleSelectionModel<Tab>>() {

            @Override
            public void changed(ObservableValue<? extends SingleSelectionModel<Tab>> ov,
                    SingleSelectionModel<Tab> oldValue, SingleSelectionModel<Tab> newValue) {
            }
        });

        // bind to take available space
        borderPane.prefHeightProperty().bind(scene.heightProperty());
        borderPane.prefWidthProperty().bind(scene.widthProperty());

        borderPane.setCenter(tabPane);
        root.getChildren().add(borderPane);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

}

and this is the Peek class, that chaches the Snapshots

public class Peek {
    ObservableMap<Object, Image> peeks = FXCollections.observableHashMap();

    public void set(Object key, Image preview) {
        peeks.put(key, preview);
    }

    public Image get(Object key) {
        Image preview = peeks.get(key);
        return preview;
    }

}

Solution

  • It is actually because of the popOver you show. As soon as the popOver is shown, it takes the focus, and a MouseExit event is dispatch to your tab's graphic.
    You can observe it by adding a bigger offset to your popup :

    popOver.show((Node) mouseEvent.getSource(), -15);
    

    Or simply by entering your mouse from the top of the tab instead from the bottom (the problem does not appear anymore).

    Also, as the event seems not well-synchronized (when moving your mouse fast enought from a tab to another one) I advise you to create one PopOver by tab.

    public class Main extends Application {
    
       Peek peek;
    
       Tab lastPeek;
    
       HashMap<Label, PopOver> popMap = new HashMap<>();
    
       boolean show = false;
    
       public static void main(String[] args) {
          System.out.println("WE HAVE PROBLEMS WITH FOCUS HERE");
          Application.launch(args);
       }
    
       @Override
       public void start(Stage primaryStage) {
          peek = new Peek();
          primaryStage.setTitle("Tabs");
          Group root = new Group();
          Scene scene = new Scene(root, 400, 250, Color.WHITE);
    
          TabPane tabPane = new TabPane();
    
          BorderPane borderPane = new BorderPane();
    
          for(int i = 0; i < 5; i++) {
             Tab tab = new Tab();
             HBox hbox = new HBox();
             hbox.getChildren()
                   .add(new Label("", new ImageView(new Image(Main.class.getResourceAsStream(i + ".jpg")))));
             hbox.setAlignment(Pos.CENTER);
             tab.setContent(hbox);
             tabPane.getTabs().add(tab);
    
             final Label decoration = new Label("Tab" + i);
             tab.setGraphic(decoration);
             System.out.println("tab.getGraphic() = " + tab.getGraphic());
             final PopOver popOver = new PopOver();
             popOver.setArrowLocation(ArrowLocation.TOP_LEFT);
             final ImageView preview = new ImageView();
             preview.setFitHeight(100);
             preview.setSmooth(true);
             preview.setPreserveRatio(true);
             popOver.setContentNode(preview);
    
             popMap.put(decoration, popOver);
             final int index = i;
             decoration.setOnMouseEntered(mouseEvent -> {
    
                PopOver lPop = popMap.get((Label) mouseEvent.getSource());
    
                boolean show = false;
                if(lastPeek != tab) {
                   // Show PopOver when mouse enters label
                   Image previewImage = peek.get(tab);
                   if(previewImage != null) {
                      show = true;
                   }
                   System.out.println("previewImage = " + previewImage);
                   preview.setImage(previewImage);
                   System.out.println("show() " + "Tab" + index);
    
                }
                if(!tab.isSelected() && !tab.isDisabled() && show) {
                   lPop.show((Node) mouseEvent.getSource(), -decoration.getHeight() / 2);
                }
             });
    
             tab.getGraphic().setOnMouseExited(mouseEvent -> {
                // Hide PopOver when mouse exits label
                // how can we handle this correct??
                PopOver lPop = popMap.get((Label) mouseEvent.getSource());
                if(lPop.isShowing())
                   lPop.hide();
                System.out.println("hide() " + "Tab" + index);
             });
    
          }
    
          tabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() {
    
             @Override
             public void changed(ObservableValue<? extends Tab> ov, Tab oldTab, Tab newTab) {
                Image preview = oldTab.getContent().snapshot(new SnapshotParameters(), null);
                peek.set(oldTab, preview);
             }
          });
          tabPane.selectionModelProperty().addListener(new ChangeListener<SingleSelectionModel<Tab>>() {
    
             @Override
             public void changed(ObservableValue<? extends SingleSelectionModel<Tab>> ov,
                   SingleSelectionModel<Tab> oldValue, SingleSelectionModel<Tab> newValue) {
             }
          });
    
          // bind to take available space
          borderPane.prefHeightProperty().bind(scene.heightProperty());
          borderPane.prefWidthProperty().bind(scene.widthProperty());
    
          borderPane.setCenter(tabPane);
          root.getChildren().add(borderPane);
          primaryStage.setScene(scene);
          primaryStage.show();
       }
    }