Custom Border for Callout Popup

Richard Bair richard.bair at oracle.com
Tue May 8 15:25:28 PDT 2012


Hi Werner,

Here is an updated Skin which works a bit better. What I added here I found in the TooltipSkin class, which you can peruse as it is open source. It uses the root node of the skin to do the layout of the background shape (which for simplicity I used a Rectangle, you could either use a path with known path elements that you reposition during layout or you can recreate the path elements each time).

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package mint.javafx.stage;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.PathBuilder;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeType;

/**
 *
 * @author richardbair
 */
public class MintCalloutPopup2Skin implements Skin<MintCalloutPopup2>
{
  private MintCalloutPopup2 callout;
  private Rectangle backgroundShape;
  private StackPane root;

  public MintCalloutPopup2Skin(final MintCalloutPopup2 callout) {
    this.callout = callout;
    backgroundShape = (Rectangle) createCalloutShape();
    root = new StackPane() {
        @Override protected void layoutChildren() {
            final Insets insets = getInsets();
            final double x = insets.getLeft();
            final double y = insets.getTop();
            final double width = getWidth() - insets.getLeft() - insets.getRight();
            final double height = getHeight() - insets.getTop() - insets.getBottom();
            
            backgroundShape.setX(0);
            backgroundShape.setY(0);
            backgroundShape.setWidth(getWidth());
            backgroundShape.setHeight(getHeight());
            
            Node content = callout.getContentNode();
            if (content != null) {
                content.resizeRelocate(x, y, width, height);
            }
        }

        @Override protected double computeMinWidth(double width) {
            return (callout.getMinWidth() != -1) ? callout.getMinWidth() : computePrefWidth(width);
        }

        @Override protected double computeMinHeight(double height) {
            return (callout.getMinHeight() != -1) ? callout.getMinHeight() : computePrefHeight(height);
        }

        @Override protected double computePrefWidth(double width) {
            if(callout.getPrefWidth() != -1 ) {
                return callout.getPrefWidth();
            }
            
            final Node content = callout.getContentNode();
            final double contentWidth =
                    content == null ? 0 : content.prefWidth(-1);
            return getInsets().getLeft() + contentWidth + getInsets().getRight();
        }

        @Override protected double computePrefHeight(double height) {
            final Insets insets = getInsets();
            final Node content = callout.getContentNode();                
            if (content == null) {
                return insets.getTop() + insets.getBottom();
            } else if (callout.getPrefWidth() != -1) {
                return insets.getTop() + content.prefHeight(callout.getPrefWidth() -
                        insets.getLeft() - insets.getRight()) + insets.getBottom();
            } else {
                return insets.getTop() + content.prefHeight(-1) + insets.getBottom();
            }
        }
        
        @Override protected double computeMaxWidth(double width) {
            return (callout.getMaxWidth() != -1) ? callout.getMaxWidth() : computePrefWidth(width);
        }

        @Override protected double computeMaxHeight(double height) {
            return (callout.getMaxHeight() != -1) ? callout.getMaxHeight() : computePrefHeight(height);
        }
    };
    root.getChildren().add(backgroundShape);
    root.getStyleClass().setAll(callout.getStyleClass());
    root.setStyle(callout.getStyle());
    root.setId(callout.getId());
    
    if (callout.getContentNode() != null) {
        root.getChildren().add(callout.getContentNode());
    }
    callout.contentNodeProperty().addListener(new InvalidationListener() {
        @Override public void invalidated(Observable c) {
            final Node content = callout.getContentNode();
            if (content == null) {
                root.getChildren().setAll(backgroundShape);
            } else {
                root.getChildren().setAll(backgroundShape, content);
            }
        }
    });
  }

  @Override public MintCalloutPopup2 getSkinnable() { return callout; }
  @Override public Node getNode() { return root; }
  @Override public void dispose() { callout = null; }

  private Shape createCalloutShape() {
    //MoveTo e0 ... // bound to widthProperty/heightProperty
    // ...
    //ClosePath e7 = new ClosePath();

      return new Rectangle(0, 0, 100, 100);
//    return PathBuilder.create()
//        .managed(false)
//        .strokeWidth(1)
//        .smooth(false)
//        .strokeType(StrokeType.OUTSIDE)
//        .fill(javafx.scene.paint.Color.WHITE)
//        .elements(e0, e1, e2, e3, e4, e5, e6, e7)
//        .build();
  }
}

On May 8, 2012, at 8:24 AM, Werner Lehmann wrote:

> Hi Richard,
> 
> I tried to do what you suggested. It works... sort of. After quite some code reading I ended up with the code below. The remaining main problem is that I have to use the tip of the arrow/tail as hotspot to position the callout popup.
> 
> In my original attempt I showed it offscreen to get the actual size, then moved it to the right spot. With separate control/skin classes this has become more difficult because only the skin knows how to compute that hotspot but the control does not know the skin (intentionally, I assume). Furthermore, the skin instance does not even exist before the control shows.
> 
> So, how can I measure the width before it is even visible on screen? In the skin constructor the popup width is still zero. I need the width to move the popup origin to the left because the arrow/tail hotspot is on the right side.
> 
> I have been moving code back and forth and back but could not figure this out.
> 
> Thanks...
> Werner
> 
> -------
> 
> .mint-callout-popup {
>    -fx-skin: "mint.javafx.stage.MintCalloutPopup2Skin";
>    -fx-padding: 15 5 0 5;
> }
> 
> -------
> 
> public class MintCalloutPopup2 extends PopupControl
> {
>   private static final String DEFAULT_STYLE_CLASS =
>     "mint-callout-popup";
> 
>   static {
>     StyleManager.getInstance().addUserAgentStylesheet(
>       MintCalloutPopup2.class.getResource(
>         "MintCalloutPopup2.css").toExternalForm());
>   }
> 
>   private final ObjectProperty<Node> contentNode =
>     new SimpleObjectProperty<Node>(this, "contentNode");
>   private final ObjectProperty<Node> target =
>     new SimpleObjectProperty<Node>(this, "target");
> 
>   public MintCalloutPopup2()
>   {
>     getStyleClass().setAll(DEFAULT_STYLE_CLASS);
>     setAutoHide(true);
>     setHideOnEscape(true);
>   }
> 
>   public ObjectProperty<Node> contentNodeProperty() { return contentNode; }
>   public Node getContentNode() { return contentNode.get(); }
>   public void setContentNode(Node value) { contentNode.set(value); }
> 
>   public ObjectProperty<Node> targetProperty() { return target; }
>   public Node getTarget() { return target.get(); }
>   public void setTarget(Node value) { target.set(value); }
> 
>   public void showAtTarget()
>   {
>     show(getTarget().getScene().getWindow());
>   }
> }
> 
> -------
> 
> public class MintCalloutPopup2Skin implements Skin<MintCalloutPopup2>
> {
>   private MintCalloutPopup2 callout;
>   private StackPane root;
>   private StackPane content;
> 
>   public MintCalloutPopup2Skin(MintCalloutPopup2 callout)
>   {
>     this.callout = callout;
>     content = new StackPane();
>     replaceContent();
>     callout.contentNodeProperty().addListener(
>       new InvalidationListener() {
>         @Override public void invalidated(Observable observable)
>         { replaceContent(); }
>     });
> 
>     root = new StackPane();
>     root.getChildren().addAll(createCalloutShape(), content);
>     root.getStyleClass().setAll(callout.getStyleClass());
>     root.setStyle(callout.getStyle());
>     root.setId(callout.getId());
> 
>     // <------------- the popup bounds are unknown here, how
>     //                can I relocate it "right/top aligned"?
>   }
> 
>   private void replaceContent()
>   {
>     if (callout.getContentNode() == null)
>       content.getChildren().clear();
>     else
>       content.getChildren().setAll(callout.getContentNode());
>   }
> 
>   @Override public MintCalloutPopup2 getSkinnable() { return callout; }
>   @Override public Node getNode() { return root; }
>   @Override public void dispose() { callout = null; }
> 
>   private Shape createCalloutShape()
>   {
>     MoveTo e0 ... // bound to widthProperty/heightProperty
>     // ...
>     ClosePath e7 = new ClosePath();
> 
>     return PathBuilder.create()
>         .managed(false)
>         .strokeWidth(1)
>         .smooth(false)
>         .strokeType(StrokeType.OUTSIDE)
>         .fill(javafx.scene.paint.Color.WHITE)
>         .elements(e0, e1, e2, e3, e4, e5, e6, e7)
>         .build();
>   }
> }
> 
> 
> On 07.05.2012 23:54, Richard Bair wrote:
>> I think what I would have done would be to extend from PopupControl.
>> Then write a new Skin for your Callout. The skin would add the path
>> as one of its children, and whatever content is added to the content
>> would then be added after the path. The path would have "managed" set
>> to false. Then, in the layoutChildren() method of your Callout, you
>> would reposition the path based on the current bounds and then call
>> super.layoutChildren() which would position and layout the content.
>> 
>> So something like:
>> 
>> public class Callout extends PopupControl { ... }
>> 
>> // Doh! SkinBase is a private class, but you can copy/paste it into
>> your code if it is GPL+classpath..., otherwise just see what it does
>> and write a fresh Skin that does the same public class CalloutSkin
>> extends SkinBase<Callout>  { private Path path; ... }
>> 
>> Richard
>> 
>> 
>> 
>> On May 7, 2012, at 12:32 PM, Werner Lehmann wrote:
>> 
>>> Hi,
>>> 
>>> I am creating a popup for a callout. The callout is basically a
>>> rectangle with a small arrow/tail on the top right corner and a
>>> single node for its content. That node may resize later and the
>>> callout has to adjust. http://i49.tinypic.com/21crux3.jpg
>>> 
>>> First I tried to do this with -fx-shape and an svg path but without
>>> luck: could not make this work, never saw that shape. Unfortunately
>>> I also did not find any example... I suppose even an SVG path
>>> border would not resize automatically when the content changes -
>>> otherwise this would be exactly what I need ;-)
>>> 
>>> Plan B is to use a Popup with a Path in the appropriate shape. This
>>> works but I am battling two things:
>>> 
>>> 1. I have to know the popup content layout bounds before I can
>>> position the popup "right aligned". Currently solved by showing the
>>> popup offscreen (at -10000, 0) to get valid layoutBounds. Then I
>>> move it to the correct position. Is there a better way to get
>>> layoutBounds before showing a window?
>>> 
>>> 2. When the content size changes, a similar problem arises: now I
>>> have to determine the new popup size, change the border Path to
>>> match the new size (PathElement binding might work here) and
>>> reposition the window. A resize animation would be nice, and some
>>> clipping will be needed also...
>>> 
>>> This is when I got an idea: is it possible to create and use a
>>> custom border class? Looks as if I could extend
>>> com.sun.javafx.scene.layout.region.Border... In this way I would
>>> get content clipping and resizing (position of arrow/tail must be
>>> adjusted) for free.
>>> 
>>> Any thoughts?
>>> 
>>> Thanks. Werner
>>> 
>> 
> 
> -- 
> --------------------------------------------------------
> MINT MEDIA INTERACTIVE Software Systems GmbH
> Kiel Science Centre / Wissenschaftszentrum
> Fraunhoferstr. 13
> D-24118 Kiel, Germany
> Phone: +49 (0)431 530215-0
> Fax: +49 (0)431 5302090
> Mail: lehmann at media-interactive.de
> Web: www.media-interactive.de
> --------------------------------------------------------
> --------------------------------------------------------
> Sitz der Gesellschaft / Corporate HQ: Kiel
> Reg.-eintragung / Registration:
> Amtsgericht Kiel HRB 4860
> Ust-ID Nr. DE 197455787
> Geschäftsführer / Managing Director: Jörg Latteier
> --------------------------------------------------------
> 
> 



More information about the openjfx-dev mailing list