JavaFX2 – Drag-and-drop list cell as tags to FlowPane

septembre 26th, 2014

Do you use tags to organize content on your site? This tutorial will explain how to turn your listview drag-and-drop gesture’s results into a style-able object tags with its own delete button link. To achieve this, we’ll create a Custom UI controls to implements the tags panel object. Here is an image of the Tags pane final result might look like.


Drag-and-drop gesture tags

Getting started

Every UI component in JavaFX is composed by a control, a skin and a behavior. In an ideal case there is a css part to. You must read this article for more explanations.

custom ui controls control, behavior, skin

The Control class

The control class extends javafx.scene.control.Control. It’s in charge to hold the properties of the component and acts as the main class for it because instances of this class will later created in your application code and added to the UI tree.

This class provides a tagNames ObjectProperty list, which coupled with a ListChangeListener, will listen on changes in the tags pane container (FlowPane).


    private final ObjectProperty<ObservableList<Tag>> tagNames
            = new SimpleObjectProperty(FXCollections.<Tag>observableArrayList());

Here is the code:


import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Control;

public class DraggableTagsPane<T> extends Control {
   
    private final String CSS = "/styles/" + this.getClass().getSimpleName() + ".css";    
    private final DoubleProperty panelWidth = new SimpleDoubleProperty();
    private final DoubleProperty panelHeight = new SimpleDoubleProperty();

    /**
     * The available tags name list used to listen to changes in tags pane.
     */

    private final ObjectProperty<ObservableList<Tag>> tagNames
            = new SimpleObjectProperty(FXCollections.<Tag>observableArrayList());
       
    public DraggableTagsPane(double panelWidth, double panelheight) {
        // setup the CSS
        // the -fx-skin attribute in the CSS sets which Skin class is used
        this.getStyleClass().add(this.getClass().getSimpleName().toLowerCase());        
       
        setPanelWidth(panelWidth);
        setPanelHeight(panelheight);        
    }

    @Override
    protected String getUserAgentStylesheet() {
        return this.getClass().getResource(CSS).toString();
    }    
   
    public DoubleProperty panelWidthProperty() {
        return panelWidth;
    }

    public Double getPanelWidth() {
        return panelWidth.get();
    }

    public void setPanelWidth(Double panelWidth) {
        this.panelWidth.set(panelWidth);
    }
   
    public DoubleProperty panelHeightProperty() {
        return panelHeight;
    }

    public Double getPanelHeight() {
        return panelHeight.get();
    }

    public void setPanelHeight(Double panelHeight) {
        this.panelHeight.set(panelHeight);
    }    
   
    public ObjectProperty<ObservableList<Tag>> tagNamesProperty() {
        return tagNames;
    }

    public ObservableList<Tag> getTagNames() {
        return tagNames.get();
    }

    public void setTagNames(ObservableList<Tag> tagNames) {
        this.tagNames.set(tagNames);
    }    
}

The CSS

Inside the css you have to specify a new selector for your component and add the skin as a property to it. The full css example look like this:


.draggabletagspane {
    -fx-skin: "com.physalix.jfx.skin.DraggableTagsPaneSkin";
}

.draggable-tag {
    -fx-font: 12pt "Arial";
    -fx-text-alignment: center;    
}

.tags-scrollpane, .tags-scroll {
    -fx-background-color: orange;      
}

.draggable-tag, .tags-region {
    -fx-background-color: transparent;
}

.closetag .button {
    -fx-background-color: white, white;
    -fx-background-radius: 16.4, 15;
    -fx-border-radius: 20;
    -fx-border-width: 1;
    -fx-border-color: #D8D8D8;
    -fx-padding: 0;
    -fx-font-size: 0;
}

.closetag .button:focused {
    -fx-background-color: -fx-focus-color, white;
}

.closetext {
    -fx-stroke: #A4A4A4;
    -fx-fill: #A4A4A4;
    -fx-font-size: 7;
}

.closeicon {
    -fx-stroke: #D8D8D8;
    -fx-stroke-width: 2;
}

.closetag:hover {
    -fx-effect: innershadow(gaussian, white, 15, 0, 0, 0);
}

The Behavior class

The Behavior class will extend com.sun.javafx.scene.control.behavior.BehaviorBase. It use another JavaFX collection other than the one declared in the Control class. Each time, this collection change, the Control’s tagNames collection is updated and new Tags added to the Tagspane (FlowPane). This way, it will be possible to add a ListChangeListener to the Control’s tagNames collection outside of the Custom control.

The drag-and-drop gesture target is also implemented in the Behavior and belong to the ScrollPane available in the Skin class.

The drag-and-drop gesture happens as follows: The user a cell list string on a gesture source, drags the mouse, and releases the mouse button over the gesture target (ScrollPane). The data is transferred using a dragboard.

Here is the code:


import com.physalix.jfx.DraggableTagsPane;
import com.physalix.jfx.Tag;
import com.physalix.jfx.skin.DraggableTagsPaneSkin;
import com.sun.javafx.scene.control.behavior.BehaviorBase;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;

public class DraggableTagsPaneBehavior<T> extends BehaviorBase<DraggableTagsPane<T>> {

    /**
     * The second tag name list used in the Behavior.
     */

    private final ObservableList<Tag> tagsList = FXCollections.observableArrayList();

    /**
     * Control skin.
     */

    private DraggableTagsPaneSkin<T> skin;

    /**
     * Control properties.
     */

    private DraggableTagsPane control;

    public DraggableTagsPaneBehavior(DraggableTagsPane tagsControl) {
        super(tagsControl);
    }

    private final ListChangeListener<Tag> dataChangeListener = new ListChangeListener<Tag>()   {
        @Override
        public void onChanged(
                ListChangeListener.Change<? extends Tag> c) {

            while (c.next()) {
                tagsList.removeAll(c.getRemoved());
                control.getTagNames().removeAll(c.getRemoved());
                control.getTagNames().addAll(c.getAddedSubList());

                for (Tag item : c.getAddedSubList()) {
                    skin.getTagsPane().getChildren().add(item);
                }
            }
        }
    };

    /**
     * Create the onDragOver tags event.
     */

    private final EventHandler<DragEvent> onDragOverTagsEventHandler
            = new EventHandler<DragEvent>() {

                @Override
                public void handle(DragEvent event) {
                    // data is dragged over the target
                    Dragboard db = event.getDragboard();

                    if (event.getGestureSource() != skin.getScrollPane()
                    && db.hasString()) {
                        event.acceptTransferModes(TransferMode.COPY);
                    }
                    event.consume();
                }
            };

    /**
     * Create the onDragDrop tags event.
     */

    private final EventHandler<DragEvent> onDragDropTagsEventHandler
            = new EventHandler<DragEvent>() {

                @Override
                public void handle(DragEvent event) {
                    Dragboard db = event.getDragboard();

                    if (event.getGestureSource() != skin.getScrollPane()
                    && db.hasString()) {
                        event.acceptTransferModes(TransferMode.COPY);
                        tagsList.add(new Tag(db.getString(),
                                        new CloseTagEventHandler()));
                    }
                    event.consume();
                }
            };


    public void initialize(DraggableTagsPaneSkin<T> skin) {
        this.skin = skin;
        control = getControl();
        tagsList.addListener(dataChangeListener);
        skin.getScrollPane().setOnDragOver(onDragOverTagsEventHandler);
        skin.getScrollPane().setOnDragDropped(onDragDropTagsEventHandler);
    }

    /**
     * Close button event handler inner class.
     */

    private class CloseTagEventHandler implements EventHandler<ActionEvent> {

        @Override
        public void handle(final ActionEvent event) {

            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    Node tag = ((Node) event.getSource()).getParent()
                            .getParent().getParent();

                    skin.getTagsPane().getChildren().remove((Tag) tag);
                    // update tagsList
                    tagsList.remove((Tag) tag);
                }
            });
        }

    }

}

The Skin class

Compared to Swing, JavaFX goes one step further and provides the skin class for all visualization and layout related code for all user interaction. The SkinBase class will mostly extend com.sun.javafx.scene.control.skin.BaseSkin, but offered a lot more convenience, in particular the concept of a Behavior.

The container used to store dropped’s tags is the horizontal FlowPane that will layout node (tags) in rows, wrapping them at the flowpane’s width.


import com.physalix.jfx.DraggableTagsPane;
import com.physalix.jfx.behavior.DraggableTagsPaneBehavior;
import com.sun.javafx.scene.control.skin.SkinBase;
import javafx.geometry.Orientation;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.FlowPane;

public class DraggableTagsPaneSkin<T> extends SkinBase<DraggableTagsPane<T>, DraggableTagsPaneBehavior<T>> {

   
    private final FlowPane tagsPane = new FlowPane(Orientation.HORIZONTAL);
    private final ScrollPane scrollPane = new ScrollPane();
   
    public DraggableTagsPaneSkin(DraggableTagsPane control) {
        super(control, new DraggableTagsPaneBehavior(control));
        initialize();
    }

    /**
     * Initializes this skin.
     */
   
    private void initialize() {
        final DraggableTagsPane control = getSkinnable();
       
        getBehavior().initialize(this);
       
        getScrollPane().getStyleClass().add("tags-scroll");
        getScrollPane().setFitToHeight(false);
        getScrollPane().setFitToWidth(true);
        getScrollPane().setPrefHeight(control.getPanelHeight());
        getScrollPane().setPrefWidth(control.getPanelWidth());

        getTagsPane().getStyleClass().add("tags-region");

        AnchorPane.setBottomAnchor(getTagsPane(), 0.0);
        AnchorPane.setRightAnchor(getTagsPane(), 0.0);
        AnchorPane.setTopAnchor(getTagsPane(), 0.0);
        AnchorPane.setLeftAnchor(getTagsPane(), 0.0);

        AnchorPane tagsScrollPane = new AnchorPane();
        tagsScrollPane.setPrefHeight(control.getPanelHeight());
        tagsScrollPane.setPrefWidth(control.getPanelWidth());
        tagsScrollPane.getStyleClass().add("tags-scrollpane");

        AnchorPane.setBottomAnchor(tagsScrollPane, 0.0);
        AnchorPane.setRightAnchor(tagsScrollPane, 0.0);
        AnchorPane.setTopAnchor(tagsScrollPane, 0.0);
        AnchorPane.setLeftAnchor(tagsScrollPane, 0.0);

        tagsScrollPane.getChildren().add(getTagsPane());
        getScrollPane().setContent(tagsScrollPane);
       
        getChildren().add(getScrollPane());
    }

    public FlowPane getTagsPane() {
        return tagsPane;
    }

    public ScrollPane getScrollPane() {
        return scrollPane;
    }
}

The Tag class

This class is the implementation of the tag itself.


import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBuilder;

public class Tag extends Region {

    private final String name;
   
    public Tag(String name, EventHandler<ActionEvent> actionHandler) {
        this.name = name;
       
        getStyleClass().add("draggable-tag");

        Text label = new Text();
        label.setTextAlignment(TextAlignment.CENTER);
        label.setFill(Color.BLACK);
        label.setTextOrigin(VPos.CENTER);
        label.setFont(Font.font("Arial", 12));
        label.setX(25);
        label.setY(14);
        label.setText(name);

        Rectangle rect = new Rectangle(
                label.getLayoutBounds().getWidth() + 35, 25);
        rect.setFill(Color.web("D8D8D8"));
        rect.setX(2);
        rect.setY(1);
        rect.setArcWidth(10);
        rect.setArcHeight(10);

        Group group = new Group();
        group.getChildren().addAll(rect, label,
                closeButton(textCloseIcon(), actionHandler));

        setPrefHeight(28);
        getChildren().addAll(group);
    }

    /**
     * Create the tag’s close button.
     */

    private Region closeButton(Node closeIcon,
            EventHandler<ActionEvent> actionHandler) {

        final double BUTTON_HEIGHT = 15;
        final double BUTTON_WIDTH = 15;

        StackPane closeButton = new StackPane();
        closeButton.getStyleClass().add("closetag");
        Button button = new Button();
        button.setOnAction(actionHandler);

        closeIcon.setMouseTransparent(true);

        closeButton.getChildren().addAll(button, closeIcon);
        button.setMinSize(BUTTON_WIDTH, BUTTON_HEIGHT);
        closeButton.setPrefSize(BUTTON_WIDTH, BUTTON_HEIGHT);
        closeButton.setMaxSize(StackPane.USE_PREF_SIZE, StackPane.USE_PREF_SIZE);
        closeButton.setTranslateY(5);
        closeButton.setTranslateX(5);

        return closeButton;
    }

    /**
     * Add close text (x) in a Circle.
     */

    private Node textCloseIcon() {
        return TextBuilder.create().text("x")
                .styleClass("closetext").build();
    }

    public String getName() {
        return name;
    }

}

The Main class

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class Main extends Application {

    private static final String STYLESHEET_PATH = "/styles/styles.css";

    private static final ObservableList<String> data
            = FXCollections.observableArrayList(
                    "chocolate", "salmon", "gold", "coral", "darkorchid",
                    "darkgoldenrod", "lightsalmon", "black", "rosybrown", "blue",
                    "blueviolet", "brown");

    private final ListView<String> listView = new ListView();

    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(new Group());
        scene.getStylesheets().add(STYLESHEET_PATH);
        stage.setTitle("Draggable Tags Panel Sample");
        stage.setWidth(600);
        stage.setHeight(400);

        AnchorPane anchorPane = new AnchorPane();
        anchorPane.setPrefHeight(400);
        anchorPane.setPrefWidth(600);

        listView.setLayoutX(14.0);
        listView.setLayoutY(14.0);
        listView.setPrefHeight(300.0);
        listView.setPrefWidth(165.0);

        listView.setItems(data);
        listView.setOnDragDetected(onDragListDetectedEventHandler);

        DraggableTagsPane tagPane = new DraggableTagsPane(371.0, 300.0);
        tagPane.setLayoutX(200.0);
        tagPane.setLayoutY(14.0);

        tagPane.getTagNames().addListener(itemChangeListener);

        anchorPane.getChildren().addAll(listView, tagPane);

        ((Group) scene.getRoot()).getChildren().addAll(anchorPane);

        stage.setScene(scene);
        stage.show();
    }

    /**
     *  The listchange listener in charge to listen on changes in the Control tagsName list’s class.
     */

    private final ListChangeListener<Tag> itemChangeListener = new ListChangeListener<Tag>() {
        @Override
        public void onChanged(
                ListChangeListener.Change<? extends Tag> c) {

            while (c.next()) {
                for (Tag tag : c.getRemoved()) {
                    System.out.println("REMOVED: " + tag.getName());
                }
                for (Tag tag : c.getAddedSubList()) {
                    System.out.println("ADDED: " + tag.getName());
                }
            }
        }
    };

    /**
     * Create the onDragDetected tags event for the listview.
     */

    private final EventHandler<MouseEvent> onDragListDetectedEventHandler
            = new EventHandler<MouseEvent>() {

                @Override
                public void handle(MouseEvent event) {
                    // drag was detected, start drag-and-drop gesture
                    String item = listView.getItems().get(listView.getSelectionModel().getSelectedIndex());

                    if (item != null) {
                        Dragboard db = listView.startDragAndDrop(TransferMode.COPY);

                        ClipboardContent content = new ClipboardContent();
                        content.putString(item);
                        db.setContent(content);
                        event.consume();
                    }
                }
            };

    public static void main(String[] args) {
        launch(args);
    }
}

The source code of this sample is available on GitHub.

  • fernando andrauss

    Awesome! folowing the blog.