/*
* Copyright [2014] [Christian Loehnert, krampenschiesser@gmail.com]
* Licensed 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 de.ks.idnadrev.task.view;
import de.ks.BaseController;
import de.ks.datasource.DataSource;
import de.ks.i18n.Localized;
import de.ks.idnadrev.entity.Context;
import de.ks.idnadrev.entity.Task;
import de.ks.idnadrev.entity.TaskState;
import de.ks.persistence.PersistentWork;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import org.controlsfx.control.PopOver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class ViewTasksMaster extends BaseController<List<Task>> {
private static final Logger log = LoggerFactory.getLogger(ViewTasksMaster.class);
@FXML
protected TreeTableView<Task> tasksView;
@FXML
protected TreeTableColumn<Task, Task> taskViewNameColumn;
@FXML
protected TreeTableColumn<Task, String> taskViewEstimatedTimeColumn;
@FXML
protected TreeTableColumn<Task, String> taskViewCreationTimeColumn;
@FXML
protected Button moreBtn;
@FXML
protected TextField searchField;
@FXML
protected ComboBox<String> contextSelection;
protected final ObservableList<Task> tasks = FXCollections.observableArrayList();
private Map<Task, TreeItem<Task>> task2TreeItem = new HashMap<>();
private final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern(Localized.get("fullDate"));
private Predicate<Task> filter = t -> true;
private PopOver popOver;
private ChangeListener<Boolean> hideOnFocusLeave;
@Override
public void initialize(URL location, ResourceBundle resources) {
ChangeListener<String> listener = (observable, oldValue, newValue) -> {
refreshFilter();
};
searchField.textProperty().addListener(listener);
contextSelection.getSelectionModel().selectedItemProperty().addListener(listener);
taskViewNameColumn.setCellFactory(param -> {
TreeTableCell<Task, Task> cell = new TreeTableCell<Task, Task>() {
@Override
protected void updateItem(Task item, boolean empty) {
super.updateItem(item, empty);
String styleClassFinished = "taskViewFinished";
String styleClassASAP = "taskViewAsap";
getTreeTableRow().getStyleClass().remove(styleClassFinished);
getTreeTableRow().getStyleClass().remove(styleClassASAP);
if (item != null) {
setText(item.getName());
if (item.isFinished()) {
getTreeTableRow().getStyleClass().add(styleClassFinished);
}
if (item.getState() == TaskState.ASAP) {
getTreeTableRow().getStyleClass().add(styleClassASAP);
}
} else {
setText("");
}
}
};
return cell;
});
taskViewNameColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getValue()));
taskViewEstimatedTimeColumn.setCellValueFactory(param -> new SimpleStringProperty(parseDuration(param.getValue().getValue().getEstimatedTime(), false)));
taskViewCreationTimeColumn.setCellValueFactory(param -> {
TreeItem<Task> treeItem = param.getValue();
Task task = treeItem.getValue();
LocalDateTime creationTime = task.getCreationTime();
String formatted = dateFormat.format(creationTime);
return new SimpleStringProperty(formatted);
});
CompletableFuture.supplyAsync(() -> PersistentWork.from(Context.class).stream().map(c -> c.getName()).collect(Collectors.toList()), controller.getExecutorService())//
.thenAcceptAsync(contextNames -> {
ObservableList<String> items = FXCollections.observableArrayList(contextNames);
items.add(0, "");
items.add(1, Localized.get("all"));
contextSelection.setItems(items);
contextSelection.getSelectionModel().select(1);
}, controller.getJavaFXExecutor());
searchField.setOnKeyReleased(e -> {
if (e.getCode() == KeyCode.ESCAPE) {
searchField.setText("");
e.consume();
}
});
this.hideOnFocusLeave = (fp, fo, fn) -> {
if (!fn && popOver != null) {
boolean needsToKeepFocus = activityInitialization.getControllerInstance(TaskFilterView.class).needsToKeepFocus();
if (!needsToKeepFocus) {
popOver.hide();
}
}
};
moreBtn.sceneProperty().addListener((p, o, n) -> {
if (n == null && popOver != null) {
popOver.hide();
if (o != null) {
o.getWindow().focusedProperty().removeListener(this.hideOnFocusLeave);
}
} else if (n != null) {
ReadOnlyBooleanProperty focused = moreBtn.getScene().getWindow().focusedProperty();
log.info("Hiding popover because focus left scene");
focused.removeListener(this.hideOnFocusLeave);
focused.addListener(this.hideOnFocusLeave);
} else if (n == null && o != null) {
o.getWindow().focusedProperty().removeListener(this.hideOnFocusLeave);
}
});
}
protected void refreshFilter() {
filter = createFilter();
TreeItem<Task> root = buildTreeStructure(new ArrayList<>(tasks));
tasksView.setRoot(root);
selectBest(root);
}
protected Predicate<Task> createFilter() {
return task -> {
boolean forceNullFilter = contextSelection.getValue() != null && contextSelection.getValue().trim().isEmpty();
Predicate<Context> filter;
if (contextSelection.getValue() == null || contextSelection.getValue().trim().equals(Localized.get("all"))) {
filter = null;
} else {
filter = ctx -> {
if (forceNullFilter) {
return ctx == null;
} else {
return ctx.getName().equals(contextSelection.getValue().trim());
}
};
}
if (filter != null) {
Context taskContext = task.getContext();
if (taskContext == null) {
boolean foundMatchingContext = false;
for (Task current = task; current.getParent() != null; current = current.getParent()) {
Context parentContext = current.getParent().getContext();
try {
if (parentContext != null && filter.test(parentContext)) {
foundMatchingContext = true;
}
} catch (Exception e) {
log.error("Could not get context of {}", current.getParent().getName());
throw e;
}
}
if (!foundMatchingContext) {
return false;
}
} else if (!forceNullFilter && !taskContext.getName().equals(contextSelection.getValue().trim())) {
return false;
}
}
String nameSearch = searchField.textProperty().getValueSafe().trim().toLowerCase(Locale.ROOT);
if (!nameSearch.isEmpty()) {
if (task.getName().toLowerCase(Locale.ROOT).contains(nameSearch)) {
return true;
} else {
return false;
}
}
return true;
};
}
private String parseDuration(Duration duration, boolean useShortFormat) {
if (duration == null) {
return null;
} else {
long hours = duration.toHours();
if (hours == 0 && useShortFormat) {
return duration.toMinutes() + Localized.get("duration.minutes");
} else {
long remainingMinutes = duration.minus(Duration.ofHours(hours)).toMinutes();
return String.format("%02d", hours) + ":" + String.format("%02d", remainingMinutes) + Localized.get("duration.hours.short");
}
}
}
@FXML
void onTableKeyReleased(KeyEvent event) {
if (event.getCode().equals(KeyCode.ENTER)) {
// startWork();
}
}
@FXML
public void showMoreFilters() {
TaskFilterView filter = activityInitialization.getControllerInstance(TaskFilterView.class);
Node filterView = activityInitialization.getViewForController(TaskFilterView.class);
popOver = new PopOver(filterView);
popOver.setDetachable(true);
popOver.setDetached(true);
popOver.setCornerRadius(4);
popOver.show(moreBtn);
}
@Override
protected void onRefresh(List<Task> loaded) {
tasks.clear();
tasks.addAll(loaded);
TreeItem<Task> root = buildTreeStructure(loaded);
tasksView.setRoot(root);
selectBest(root);
}
private void selectBest(TreeItem<Task> root) {
DataSource noncast = store.getDatasource();
ViewTasksDS datasource = (ViewTasksDS) noncast;
Task taskToSelect = datasource.getTaskToSelect();
Platform.runLater(() -> {
if (!root.getChildren().isEmpty()) {
root.setExpanded(true);
TreeItem<Task> treeItem = root.getChildren().get(0);
if (taskToSelect != null && task2TreeItem.containsKey(taskToSelect)) {
treeItem = task2TreeItem.get(taskToSelect);
} else {
Optional<Task> first = task2TreeItem.keySet().stream().filter(filter).findFirst();
if (first.isPresent()) {
treeItem = task2TreeItem.get(first.get());
}
}
expandParents(treeItem);
tasksView.getSelectionModel().select(treeItem);
}
});
}
private void expandParents(TreeItem<Task> treeItem) {
for (; treeItem.getParent() != null; treeItem = treeItem.getParent()) {
treeItem.setExpanded(true);
}
}
protected TreeItem<Task> buildTreeStructure(List<Task> loaded) {
TreeItem<Task> root = new TreeItem<>(new Task(Localized.get("all")) {
{
id = -1L;
}
});
task2TreeItem = new HashMap<>(loaded.size());
calculateTotalTime(loaded, root);
loaded.forEach((task) -> {
TreeItem<Task> treeItem = new TreeItem<>(task);
task2TreeItem.put(task, treeItem);
});
loaded.stream().filter(filter).sorted((o1, o2) -> o1.getName().compareTo(o2.getName())).forEach((task) -> {
for (; task.getParent() != null; task = task.getParent()) {
task2TreeItem.putIfAbsent(task.getParent(), new TreeItem<>(task.getParent()));
TreeItem<Task> parentItem = task2TreeItem.get(task.getParent());
TreeItem<Task> childItem = task2TreeItem.get(task);
if (!parentItem.getChildren().contains(childItem)) {
parentItem.getChildren().add(childItem);
}
}
TreeItem<Task> treeItem = task2TreeItem.get(task);
if (!root.getChildren().contains(treeItem)) {
root.getChildren().add(treeItem);
}
});
return root;
}
private void calculateTotalTime(List<Task> loaded, TreeItem<Task> root) {
Duration total = Duration.ofHours(0);
for (Task task : loaded) {
total = total.plus(task.getEstimatedTime());
}
root.getValue().setEstimatedTime(total);
}
public TreeTableView<Task> getTasksView() {
return tasksView;
}
public TreeItem<Task> getTreeItem(Task parent) {
return task2TreeItem.get(parent);
}
public ObservableList<Task> getTasks() {
return tasks;
}
public String getSelectedContext() {
return contextSelection.getSelectionModel().getSelectedItem();
}
public ComboBox<String> getContextSelection() {
return contextSelection;
}
}