All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jfoenix.skins.JFXChipViewSkin Maven / Gradle / Ivy

There is a newer version: 9.0.10
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.jfoenix.skins;

import com.jfoenix.controls.JFXAutoCompletePopup;
import com.jfoenix.controls.JFXChip;
import com.jfoenix.controls.JFXChipView;
import com.jfoenix.controls.JFXDefaultChip;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
import javafx.stage.Window;
import javafx.util.StringConverter;

import java.util.List;

/**
 * JFXChipArea is the material design implementation of chip Input.
 * An easy way to manage chips in a text area component with an x to
 * omit the chip.
 *
 * @author Shadi Shaheen & Gerard Moubarak
 * @version 1.0.0
 * @since 2018-02-01
 */
public class JFXChipViewSkin extends SkinBase> {

    private static final PseudoClass PSEUDO_CLASS_ERROR = PseudoClass.getPseudoClass("error");

    private CustomFlowPane root;
    private JFXChipView control;
    private TextArea inputField;
    private ChipsAutoComplete autoCompletePopup;

    private boolean moveToNewLine = false;
    private boolean editorOnNewLine = true;
    private double availableWidth;
    private double requiredWidth;

    private final ListChangeListener chipsChangeListeners = change -> {
        while (change.next()) {
            for (T item : change.getRemoved()) {
                for (int i = root.getChildren().size() - 2; i >= 0; i--) {
                    Node child = root.getChildren().get(i);
                    if (child instanceof JFXChip) {
                        if (((JFXChip) child).getItem() == item) {
                            root.getChildren().remove(i);
                            break;
                        }
                    }
                }
            }
            for (T item : change.getAddedSubList()) {
                createChip(item);
            }
        }
    };

    public JFXChipViewSkin(JFXChipView control) {
        super(control);
        this.control = control;
        root = new CustomFlowPane();
        root.getStyleClass().add("chips-pane");
        setupEditor();
        getChildren().add(root);

        // init auto complete
        autoCompletePopup = (ChipsAutoComplete) getSkinnable().getAutoCompletePopup();
        autoCompletePopup.setSelectionHandler(event -> {
            getSkinnable().getChips().add(event.getObject());
            inputField.clear();
        });
        // add position listener to auto complete
        autoCompletePopup.setShift(root.getVgap() * 2);
        root.vgapProperty().addListener((observable -> autoCompletePopup.setShift(root.getVgap() * 2)));

        // create initial chips
        for (T item : control.getChips()) {
            createChip(item);
        }
        control.getChips().addListener(new WeakListChangeListener<>(chipsChangeListeners));
    }

    private void setupEditor() {
        inputField = new TextArea();
        inputField.setManaged(false);
        inputField.getStyleClass().add("editor");
        final StringConverter sc = control.getConverter();
        if (inputField instanceof TextInputControl) {
            TextInputControl editor = (TextInputControl) inputField;
            editor.addEventFilter(KeyEvent.KEY_RELEASED, event -> {
                if (event.getCode() != KeyCode.ENTER) {
                    getSkinnable().pseudoClassStateChanged(PSEUDO_CLASS_ERROR, false);
                }
            });
            if (editor instanceof TextArea) {
                ((TextArea) editor).setWrapText(true);
                editor.textProperty().addListener(observable -> {
                    // 13 is the default scroll bar width
                    requiredWidth = editor.snappedLeftInset() + computeTextContentWidth(editor) + editor.snappedRightInset() + 13;
                    if (availableWidth < requiredWidth && !editorOnNewLine) {
                        moveToNewLine = true;
                        root.updateEditorPosition();
                        root.requestLayout();
                    } else if (availableWidth > requiredWidth && editorOnNewLine) {
                        moveToNewLine = false;
                        root.updateEditorPosition();
                        root.requestLayout();
                    }
                });
            }
            editor.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
                if (event.getCode() == KeyCode.ENTER) {
                    if (!editor.getText().trim().isEmpty()) {
                        try {
                            getSkinnable().getChips().add(sc.fromString(editor.getText()));
                            editor.clear();
                            autoCompletePopup.hide();
                        } catch (Exception ex) {
                            getSkinnable().pseudoClassStateChanged(PSEUDO_CLASS_ERROR, true);
                        }
                    }
                    event.consume();
                }
                if (event.getCode() == KeyCode.BACK_SPACE) {
                    ObservableList chips = getSkinnable().getChips();
                    int size = chips.size();
                    if ((size > 0) && editor.getText().isEmpty()) {
                        chips.remove(size - 1);
                        if (autoCompletePopup.isShowing()) {
                            autoCompletePopup.hide();
                        }
                    }
                }
            });
            editor.textProperty().addListener(observable -> {
                autoCompletePopup.filter(item -> getSkinnable().getPredicate().test(item, inputField.getText()));
                if (autoCompletePopup.getFilteredSuggestions().isEmpty()) {
                    autoCompletePopup.hide();
                } else {
                    autoCompletePopup.show(editor);
                }
            });
        }
        root.getChildren().add(inputField);
    }

    // these methods are called inside the chips items change listener
    private void createChip(T item) {
        JFXChip chip;
        if (getSkinnable().getChipFactory() != null) {
            chip = getSkinnable().getChipFactory().apply(getSkinnable(), item);
        } else {
            chip = new JFXDefaultChip(getSkinnable(), item);
        }
        int size = root.getChildren().size();
        root.getChildren().add(size - 1, chip);
    }

    private double computeTextContentWidth(TextInputControl editor) {
        Text text = new Text(editor.getText());
        text.setFont(editor.getFont());
        text.applyCss();
        return text.getLayoutBounds().getWidth();
    }


    private class CustomFlowPane extends FlowPane {
        double initOffset = 8;
        double childHeight = 0;

        @Override
        protected void layoutChildren() {
            super.layoutChildren();
            updateEditorPosition();
        }

        private VPos getRowValignmentInternal() {
            VPos localPos = getRowValignment();
            return localPos == null ? VPos.CENTER : localPos;
        }

        private HPos getColumnHalignmentInternal() {
            HPos localPos = getColumnHalignment();
            return localPos == null ? HPos.LEFT : localPos;
        }

        public void updateEditorPosition() {
            final Insets insets = getInsets();
            final double width = getWidth();
            final double height = getHeight();
            final double top = insets.getTop();
            final double left = insets.getLeft();
            final double bottom = insets.getBottom();
            final double right = insets.getRight();
            final double insideWidth = width - left - right;
            final double insideHeight = height - top - bottom;
            final double newLineEditorX = right + initOffset;

            final List managedChildren = getManagedChildren();
            final int mangedChildrenSize = managedChildren.size();
            if (mangedChildrenSize > 0) {
                Region lastChild = (Region) managedChildren.get(mangedChildrenSize - 1);
                childHeight = lastChild.getHeight();
                double contentHeight = lastChild.getHeight() + lastChild.getLayoutY();
                availableWidth = insideWidth - lastChild.getBoundsInParent().getMaxX();
                double minWidth = inputField.getMinWidth();
                minWidth = minWidth < 0 ? 100 : minWidth;
                minWidth = Math.max(minWidth, requiredWidth);

                if (availableWidth > requiredWidth) {
                    moveToNewLine = false;
                }

                if (availableWidth < minWidth || moveToNewLine) {
                    layoutInArea(inputField,
                        newLineEditorX, contentHeight + root.getVgap(),
                        insideWidth - initOffset, insideHeight - lastChild.getHeight() - lastChild.getLayoutY(),
                        0, getColumnHalignmentInternal(), VPos.TOP);
                    editorOnNewLine = true;
                } else {
                    double controlInsets = 0;
                    if (inputField instanceof TextArea) {
                        controlInsets = inputField.snappedTopInset();
                    }
                    layoutInArea(inputField,
                        lastChild.getBoundsInParent().getMaxX() + root.getHgap(),
                        lastChild.getLayoutY() + controlInsets,
                        availableWidth - root.getHgap(),
                        lastChild.getHeight(),
                        0, getColumnHalignmentInternal(), getRowValignmentInternal());
                    editorOnNewLine = false;
                }
            } else {
                layoutInArea(inputField, newLineEditorX, top, insideWidth - initOffset, height, 0, getColumnHalignmentInternal(), VPos.TOP);
                editorOnNewLine = true;
            }
        }

    }

    public static class ChipsAutoComplete extends JFXAutoCompletePopup {

        public ChipsAutoComplete() {
            getStyleClass().add("jfx-chips-popup");
        }

        private double shift = 0;

        private Text text;

        void setShift(double shift) {
            this.shift = shift;
        }

        public void show(Node node) {
            if (text == null) {
                text = (Text) node.lookup(".text");
            }
            node = text;
            if (!isShowing()) {
                if (node.getScene() == null || node.getScene().getWindow() == null) {
                    throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
                }
                Window parent = node.getScene().getWindow();
                this.show(parent, parent.getX() +
                                  node.localToScene(0, 0).getX() +
                                  node.getScene().getX(),
                    parent.getY() + node.localToScene(0, 0).getY() +
                    node.getScene().getY() + node.getLayoutBounds().getHeight() + shift);
                ((JFXAutoCompletePopupSkin) getSkin()).animate();
            } else {
                // if already showing update location if needed
                Window parent = node.getScene().getWindow();
                this.show(parent, parent.getX() +
                                  node.localToScene(0, 0).getX() +
                                  node.getScene().getX(),
                    parent.getY() + node.localToScene(0, 0).getY() +
                    node.getScene().getY() + node.getLayoutBounds().getHeight() + shift);
            }
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy