/*
* 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.concurrency.JFXUtilities;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.skins.JFXTreeTableViewSkin;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.util.Callback;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.function.Predicate;
/**
* JFXTreeTableView is the material design implementation of table view.
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09 DOC: not completed
*/
public class JFXTreeTableView<S extends RecursiveTreeObject<S>> extends TreeTableView<S> {
private TreeItem<S> originalRoot;
/**
* {@inheritDoc}
*/
public JFXTreeTableView() {
init();
}
/**
* {@inheritDoc}
*/
public JFXTreeTableView(TreeItem<S> root) {
super(root);
originalRoot = root;
init();
}
/**
* propagate any mouse event on the tree table view to its parent
*/
public void propagateMouseEventsToParent() {
this.addEventHandler(MouseEvent.ANY, e -> {
e.consume();
this.getParent().fireEvent(e);
});
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXTreeTableViewSkin<>(this);
}
protected void init() {
this.setRowFactory(param -> new JFXTreeTableRow<>());
this.getSelectionModel().selectedItemProperty().addListener((o, oldVal, newVal) -> {
if (newVal != null && newVal.getValue() != null) {
itemWasSelected = true;
}
});
this.predicate.addListener((o, oldVal, newVal) -> filter(newVal));
this.rootProperty().addListener((o, oldVal, newVal) -> {
if (newVal != null) {
setCurrentItemsCount(count(getRoot()));
}
});
// compute the current items count
setCurrentItemsCount(count(getRoot()));
}
@Override
public int getTreeItemLevel(TreeItem<?> node) {
final TreeItem<?> root = getRoot();
if (node == null) {
return -1;
}
if (node == root) {
return 0;
}
int level = 0;
TreeItem<?> parent = node.getParent();
while (parent != null) {
level++;
if (parent == root) {
break;
}
// handle group nodes
if (parent.getValue() != null
&& parent.getValue() instanceof RecursiveTreeObject
&& ((RecursiveTreeObject<?>) parent.getValue()).getGroupedColumn() != null) {
level--;
}
parent = parent.getParent();
}
return level;
}
/*
* clear selection before sorting as its bugged in java
*/
private boolean itemWasSelected = false;
/**
* {@inheritDoc}
*/
@Override
public void sort() {
getSelectionModel().clearSelection();
super.sort();
if (itemWasSelected) {
getSelectionModel().select(0);
}
}
// Allows for multiple column Grouping based on the order of the TreeTableColumns
// in this observableArrayList.
//TODO: treat group order as sort order
private ObservableList<TreeTableColumn<S, ?>> groupOrder = FXCollections.observableArrayList();
final ObservableList<TreeTableColumn<S, ?>> getGroupOrder() {
return groupOrder;
}
// semaphore is used to force mutual exclusion while group/ungroup operation
private Semaphore groupingSemaphore = new Semaphore(1);
// this method will regroup the treetableview according to columns group order
public void group(TreeTableColumn<S, ?>... treeTableColumns) {
// init groups map
if (groupingSemaphore.tryAcquire()) {
if (groupOrder.size() == 0) {
groups = new HashMap<>();
}
try {
if (originalRoot == null) {
originalRoot = getRoot();
}
for (TreeTableColumn<S, ?> treeTableColumn : treeTableColumns) {
groups = group(treeTableColumn, groups, null, (RecursiveTreeItem<S>) originalRoot);
}
groupOrder.addAll(treeTableColumns);
// update table ui
buildGroupedRoot(groups, null, 0);
} catch (Exception e) {
e.printStackTrace();
}
groupingSemaphore.release();
}
}
private void refreshGroups(List<TreeTableColumn<S, ?>> groupColumns) {
groups = new HashMap<>();
for (TreeTableColumn<S, ?> treeTableColumn : groupColumns) {
groups = group(treeTableColumn, groups, null, (RecursiveTreeItem<S>) originalRoot);
}
groupOrder.addAll(groupColumns);
// update table ui
buildGroupedRoot(groups, null, 0);
}
public void unGroup(TreeTableColumn<S, ?>... treeTableColumns) {
if (groupingSemaphore.tryAcquire()) {
try {
if (groupOrder.size() > 0) {
groupOrder.removeAll(treeTableColumns);
List<TreeTableColumn<S, ?>> grouped = new ArrayList<>();
grouped.addAll(groupOrder);
groupOrder.clear();
JFXUtilities.runInFXAndWait(() -> {
ArrayList<TreeTableColumn<S, ?>> sortOrder = new ArrayList<>();
sortOrder.addAll(getSortOrder());
// needs to reset the children in order to update the parent
List children = Arrays.asList(originalRoot.getChildren().toArray());
originalRoot.getChildren().clear();
originalRoot.getChildren().setAll(children);
// reset the original root
setRoot(originalRoot);
getSelectionModel().select(0);
getSortOrder().addAll(sortOrder);
if (grouped.size() != 0) {
refreshGroups(grouped);
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
groupingSemaphore.release();
}
}
private Map group(TreeTableColumn<S, ?> column, Map parentGroup, Object key, RecursiveTreeItem<S> root) {
if (parentGroup.isEmpty()) {
parentGroup = groupByFunction(root.filteredItems, column);
return parentGroup;
}
Object value = parentGroup.get(key);
if (value instanceof List) {
Object newGroup = groupByFunction((List) value, column);
parentGroup.put(key, newGroup);
return parentGroup;
} else if (value instanceof Map) {
for (Object childKey : ((Map) value).keySet()) {
value = group(column, (Map) value, childKey, root);
}
parentGroup.put(key, value);
return parentGroup;
} else if (key == null) {
for (Object childKey : parentGroup.keySet()) {
parentGroup = group(column, parentGroup, childKey, root);
}
return parentGroup;
}
return parentGroup;
}
protected Map groupByFunction(List<TreeItem<S>> items, TreeTableColumn<S, ?> column) {
Map<Object, List<TreeItem<S>>> map = new HashMap<>();
for (TreeItem<S> child : items) {
Object key = column.getCellData(child);
map.computeIfAbsent(key, k -> new ArrayList<>());
map.get(key).add(child);
}
return map;
}
/*
* this method is used to update tree items and set the new root
* after grouping the data model
*/
private void buildGroupedRoot(Map groupedItems, RecursiveTreeItem parent, int groupIndex) {
boolean setRoot = false;
if (parent == null) {
parent = new RecursiveTreeItem<>(new RecursiveTreeObject(), RecursiveTreeObject::getChildren);
setRoot = true;
}
for (Object key : groupedItems.keySet()) {
RecursiveTreeObject groupItem = new RecursiveTreeObject<>();
groupItem.setGroupedValue(key);
groupItem.setGroupedColumn(groupOrder.get(groupIndex));
RecursiveTreeItem node = new RecursiveTreeItem<>(groupItem, RecursiveTreeObject::getChildren);
// TODO: need to be removed once the selection issue is fixed
node.expandedProperty().addListener((o, oldVal, newVal) -> {
getSelectionModel().clearSelection();
});
parent.originalItems.add(node);
parent.getChildren().add(node);
Object children = groupedItems.get(key);
if (children instanceof List) {
node.originalItems.addAll((List) children);
node.getChildren().addAll((List) children);
} else if (children instanceof Map) {
buildGroupedRoot((Map) children, node, groupIndex + 1);
}
}
// update ui
if (setRoot) {
final RecursiveTreeItem<S> newParent = parent;
JFXUtilities.runInFX(() -> {
ArrayList<TreeTableColumn<S, ?>> sortOrder = new ArrayList<>();
sortOrder.addAll(getSortOrder());
setRoot(newParent);
getSortOrder().addAll(sortOrder);
getSelectionModel().select(0);
});
}
}
private Timer t;
/**
* this method will filter the treetable and it
*/
private void filter(Predicate<TreeItem<S>> predicate) {
if (originalRoot == null) {
originalRoot = getRoot();
}
if (t != null) {
t.cancel();
t.purge();
}
t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
// filter the original root and regroup the data
new Thread(() -> {
// filter the ungrouped root
((RecursiveTreeItem) originalRoot).setPredicate(predicate);
// regroup the data
reGroup();
Platform.runLater(() -> {
getSelectionModel().select(0);
setCurrentItemsCount(count(getRoot()));
});
}).start();
}
}, 500);
}
public void reGroup() {
if (!groupOrder.isEmpty()) {
ArrayList<TreeTableColumn<S, ?>> tempGroups = new ArrayList<>(groupOrder);
groupOrder.clear();
group(tempGroups.toArray(new TreeTableColumn[tempGroups.size()]));
}
}
private ObjectProperty<Predicate<TreeItem<S>>> predicate = new SimpleObjectProperty<>((TreeItem<S> t) -> true);
public final ObjectProperty<Predicate<TreeItem<S>>> predicateProperty() {
return this.predicate;
}
public final Predicate<TreeItem<S>> getPredicate() {
return this.predicateProperty().get();
}
public final void setPredicate(final Predicate<TreeItem<S>> predicate) {
this.predicateProperty().set(predicate);
}
private IntegerProperty currentItemsCount = new SimpleIntegerProperty(0);
private Map<Object, Map<Object, ?>> groups;
/**
* @return the initial tree items count ( add / remove items should be handled manually for now )
*/
public final IntegerProperty currentItemsCountProperty() {
return this.currentItemsCount;
}
/**
* @return the initial tree items count ( add / remove items should be handled manually for now )
*/
public final int getCurrentItemsCount() {
return this.currentItemsCountProperty().get();
}
/**
* sets the current items count
*
* @param currentItemsCount
*/
public final void setCurrentItemsCount(final int currentItemsCount) {
this.currentItemsCountProperty().set(currentItemsCount);
}
private int count(TreeItem<?> node) {
if (node == null) {
return 0;
}
int count = 1;
if (node.getValue() == null || (node.getValue() != null && node.getValue()
.getClass()
.equals(RecursiveTreeObject.class))) {
count = 0;
}
for (TreeItem<?> child : node.getChildren()) {
count += count(child);
}
return count;
}
}