/* * 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.controls; import com.jfoenix.skins.JFXListViewSkin; import com.sun.javafx.css.converters.BooleanConverter; import com.sun.javafx.css.converters.SizeConverter; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.css.*; import javafx.event.Event; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.MouseEvent; import javafx.util.Callback; import java.util.*; /** * Material design implementation of List View * * @author Shadi Shaheen * @version 1.0 * @since 2016-03-09 */ public class JFXListView<T> extends ListView<T> { /** * {@inheritDoc} */ public JFXListView() { this.setCellFactory(listView -> new JFXListCell<>()); initialize(); } /** * {@inheritDoc} */ @Override protected Skin<?> createDefaultSkin() { return new JFXListViewSkin<>(this); } private ObjectProperty<Integer> depthProperty = new SimpleObjectProperty<>(0); public ObjectProperty<Integer> depthProperty() { return depthProperty; } public int getDepth() { return depthProperty.get(); } public void setDepth(int depth) { depthProperty.set(depth); } private ReadOnlyDoubleWrapper currentVerticalGapProperty = new ReadOnlyDoubleWrapper(); ReadOnlyDoubleProperty currentVerticalGapProperty() { return currentVerticalGapProperty.getReadOnlyProperty(); } private void expand() { currentVerticalGapProperty.set(verticalGap.get()); } private void collapse() { currentVerticalGapProperty.set(0); } /* * this only works if the items were labels / strings */ private BooleanProperty showTooltip = new SimpleBooleanProperty(false); public final BooleanProperty showTooltipProperty() { return this.showTooltip; } public final boolean isShowTooltip() { return this.showTooltipProperty().get(); } public final void setShowTooltip(final boolean showTooltip) { this.showTooltipProperty().set(showTooltip); } /*************************************************************************** * * * SubList Properties * * * **************************************************************************/ @Deprecated private ObjectProperty<Node> groupnode = new SimpleObjectProperty<>(new Label("GROUP")); @Deprecated public Node getGroupnode() { return groupnode.get(); } @Deprecated public void setGroupnode(Node node) { this.groupnode.set(node); } /* * selected index property that includes the sublists */ @Deprecated private ReadOnlyObjectWrapper<Integer> overAllIndexProperty = new ReadOnlyObjectWrapper<>(-1); @Deprecated public ReadOnlyObjectProperty<Integer> overAllIndexProperty() { return overAllIndexProperty.getReadOnlyProperty(); } // private sublists property @Deprecated private ObjectProperty<ObservableList<JFXListView<?>>> sublistsProperty = new SimpleObjectProperty<>( FXCollections.observableArrayList()); @Deprecated private LinkedHashMap<Integer, JFXListView<?>> sublistsIndices = new LinkedHashMap<>(); // this method shouldn't be called from user @Deprecated void addSublist(JFXListView<?> subList, int index) { if (!sublistsProperty.get().contains(subList)) { sublistsProperty.get().add(subList); sublistsIndices.put(index, subList); subList.getSelectionModel().selectedIndexProperty().addListener((o, oldVal, newVal) -> { if (newVal.intValue() != -1) { udpateOverAllSelectedIndex(); } }); } } private void udpateOverAllSelectedIndex() { // if item from the list is selected if (this.getSelectionModel().getSelectedIndex() != -1) { int selectedIndex = this.getSelectionModel().getSelectedIndex(); Iterator<Map.Entry<Integer, JFXListView<?>>> itr = sublistsIndices.entrySet().iterator(); int preItemsSize = 0; while (itr.hasNext()) { Map.Entry<Integer, JFXListView<?>> entry = itr.next(); if (entry.getKey() < selectedIndex) { preItemsSize += entry.getValue().getItems().size() - 1; } } overAllIndexProperty.set(selectedIndex + preItemsSize); } else { Iterator<Map.Entry<Integer, JFXListView<?>>> itr = sublistsIndices.entrySet().iterator(); ArrayList<Object> selectedList = new ArrayList<>(); while (itr.hasNext()) { Map.Entry<Integer, JFXListView<?>> entry = itr.next(); if (entry.getValue().getSelectionModel().getSelectedIndex() != -1) { selectedList.add(entry.getKey()); } } if (selectedList.size() > 0) { itr = sublistsIndices.entrySet().iterator(); int preItemsSize = 0; while (itr.hasNext()) { Map.Entry<Integer, JFXListView<?>> entry = itr.next(); if (entry.getKey() < ((Integer) selectedList.get(0))) { preItemsSize += entry.getValue().getItems().size() - 1; } } overAllIndexProperty.set(preItemsSize + (Integer) selectedList.get(0) + sublistsIndices.get(selectedList .get(0)) .getSelectionModel() .getSelectedIndex()); } else { overAllIndexProperty.set(-1); } } } /*************************************************************************** * * * Stylesheet Handling * * * **************************************************************************/ /** * Initialize the style class to 'jfx-list-view'. * <p> * This is the selector class from which CSS can be used to style * this control. */ private static final String DEFAULT_STYLE_CLASS = "jfx-list-view"; private void initialize() { this.getStyleClass().add(DEFAULT_STYLE_CLASS); expanded.addListener((o, oldVal, newVal) -> { if (newVal) { expand(); } else { collapse(); } }); verticalGap.addListener((o, oldVal, newVal) -> { if (isExpanded()) { expand(); } else { collapse(); } }); // handle selection model on the list ( FOR NOW : we only support single selection on the list if it contains sublists) sublistsProperty.get().addListener((ListChangeListener.Change<? extends JFXListView<?>> c) -> { while (c.next()) { if (c.wasAdded() || c.wasUpdated() || c.wasReplaced()) { if (sublistsProperty.get().size() == 1) { this.getSelectionModel() .selectedItemProperty() .addListener((o, oldVal, newVal) -> clearSelection(this)); // prevent selecting the sublist item by clicking the right mouse button this.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume); } c.getAddedSubList() .forEach(item -> item.getSelectionModel() .selectedItemProperty() .addListener((o, oldVal, newVal) -> clearSelection(item))); } } }); // listen to index changes this.getSelectionModel().selectedIndexProperty().addListener((o, oldVal, newVal) -> { if (newVal.intValue() != -1) { udpateOverAllSelectedIndex(); } }); } // allow single selection across the list and all sublits private boolean allowClear = true; private void clearSelection(JFXListView<?> selectedList) { if (allowClear) { allowClear = false; if (this != selectedList) { this.getSelectionModel().clearSelection(); } for (int i = 0; i < sublistsProperty.get().size(); i++) { if (sublistsProperty.get().get(i) != selectedList) { sublistsProperty.get().get(i).getSelectionModel().clearSelection(); } } allowClear = true; } } /** * propagate mouse events to the parent node ( e.g. to allow dragging while clicking on the list) */ public void propagateMouseEventsToParent() { this.addEventHandler(MouseEvent.ANY, e -> { e.consume(); this.getParent().fireEvent(e); }); } private StyleableDoubleProperty cellHorizontalMargin = new SimpleStyleableDoubleProperty(StyleableProperties.CELL_HORIZONTAL_MARGIN, JFXListView.this, "cellHorizontalMargin", 0.0); public Double getCellHorizontalMargin() { return cellHorizontalMargin == null ? 0 : cellHorizontalMargin.get(); } public StyleableDoubleProperty cellHorizontalMarginProperty() { return this.cellHorizontalMargin; } public void setCellHorizontalMargin(Double margin) { this.cellHorizontalMargin.set(margin); } private StyleableDoubleProperty cellVerticalMargin = new SimpleStyleableDoubleProperty(StyleableProperties.CELL_VERTICAL_MARGIN, JFXListView.this, "cellVerticalMargin", 4.0); public Double getCellVerticalMargin() { return cellVerticalMargin == null ? 4 : cellVerticalMargin.get(); } public StyleableDoubleProperty cellVerticalMarginProperty() { return this.cellVerticalMargin; } public void setCellVerticalMargin(Double margin) { this.cellVerticalMargin.set(margin); } private StyleableDoubleProperty verticalGap = new SimpleStyleableDoubleProperty(StyleableProperties.VERTICAL_GAP, JFXListView.this, "verticalGap", 0.0); public Double getVerticalGap() { return verticalGap == null ? 0 : verticalGap.get(); } public StyleableDoubleProperty verticalGapProperty() { return this.verticalGap; } public void setVerticalGap(Double gap) { this.verticalGap.set(gap); } private StyleableBooleanProperty expanded = new SimpleStyleableBooleanProperty(StyleableProperties.EXPANDED, JFXListView.this, "expanded", false); public Boolean isExpanded() { return expanded != null && expanded.get(); } public StyleableBooleanProperty expandedProperty() { return this.expanded; } public void setExpanded(Boolean expanded) { this.expanded.set(expanded); } private static class StyleableProperties { private static final CssMetaData<JFXListView<?>, Number> CELL_HORIZONTAL_MARGIN = new CssMetaData<JFXListView<?>, Number>("-jfx-cell-horizontal-margin", SizeConverter.getInstance(), 0) { @Override public boolean isSettable(JFXListView<?> control) { return control.cellHorizontalMargin == null || !control.cellHorizontalMargin.isBound(); } @Override public StyleableDoubleProperty getStyleableProperty(JFXListView<?> control) { return control.cellHorizontalMarginProperty(); } }; private static final CssMetaData<JFXListView<?>, Number> CELL_VERTICAL_MARGIN = new CssMetaData<JFXListView<?>, Number>("-jfx-cell-vertical-margin", SizeConverter.getInstance(), 4) { @Override public boolean isSettable(JFXListView<?> control) { return control.cellVerticalMargin == null || !control.cellVerticalMargin.isBound(); } @Override public StyleableDoubleProperty getStyleableProperty(JFXListView<?> control) { return control.cellVerticalMarginProperty(); } }; private static final CssMetaData<JFXListView<?>, Number> VERTICAL_GAP = new CssMetaData<JFXListView<?>, Number>("-jfx-vertical-gap", SizeConverter.getInstance(), 0) { @Override public boolean isSettable(JFXListView<?> control) { return control.verticalGap == null || !control.verticalGap.isBound(); } @Override public StyleableDoubleProperty getStyleableProperty(JFXListView<?> control) { return control.verticalGapProperty(); } }; private static final CssMetaData<JFXListView<?>, Boolean> EXPANDED = new CssMetaData<JFXListView<?>, Boolean>("-jfx-expanded", BooleanConverter.getInstance(), false) { @Override public boolean isSettable(JFXListView<?> control) { // it's only settable if the List is not shown yet return control.getHeight() == 0 && (control.expanded == null || !control.expanded.isBound()); } @Override public StyleableBooleanProperty getStyleableProperty(JFXListView<?> control) { return control.expandedProperty(); } }; private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES; static { final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData()); Collections.addAll(styleables, CELL_HORIZONTAL_MARGIN, CELL_VERTICAL_MARGIN, VERTICAL_GAP, EXPANDED ); CHILD_STYLEABLES = Collections.unmodifiableList(styleables); } } // inherit the styleable properties from parent private List<CssMetaData<? extends Styleable, ?>> STYLEABLES; @Override public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { if (STYLEABLES == null) { final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData()); styleables.addAll(getClassCssMetaData()); styleables.addAll(ListView.getClassCssMetaData()); STYLEABLES = Collections.unmodifiableList(styleables); } return STYLEABLES; } public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { return StyleableProperties.CHILD_STYLEABLES; } }