package com.kodcu.service.ui;
import com.kodcu.controller.ApplicationController;
import com.kodcu.other.Current;
import com.kodcu.other.IOHelper;
import com.kodcu.other.Item;
import com.kodcu.service.FileWatchService;
import com.kodcu.service.PathOrderService;
import com.kodcu.service.PathResolverService;
import com.kodcu.service.ThreadService;
import javafx.application.Platform;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* Created by usta on 12.07.2014.
*/
@Component
public class FileBrowseService {
private Logger logger = LoggerFactory.getLogger(FileBrowseService.class);
private final PathOrderService pathOrder;
private final ThreadService threadService;
private final PathResolverService pathResolver;
private final AwesomeService awesomeService;
private final ApplicationController controller;
private final Current current;
@Autowired
private FileWatchService fileWatchService;
private final Map<Path, TreeItem<Item>> directoryItemMap = new ConcurrentHashMap();
private final Map<Path, TreeItem<Item>> pathItemMap = new ConcurrentHashMap();
private Set<Path> lastSelectedItems = new HashSet<>();
private PathItem rootItem;
private TreeView<Item> treeView;
private ScrollState verticalScrollState;
private ScrollState horizontalScrollState;
private Path browsedPath;
@Autowired
public FileBrowseService(final PathOrderService pathOrder, final ThreadService threadService, final PathResolverService pathResolver,
final AwesomeService awesomeService, ApplicationController controller, Current current) {
this.pathOrder = pathOrder;
this.threadService = threadService;
this.pathResolver = pathResolver;
this.awesomeService = awesomeService;
this.controller = controller;
this.current = current;
}
public void refresh() {
if (Objects.nonNull(browsedPath)) {
browse(browsedPath);
}
}
public void browse(final Path path) {
if (path != browsedPath) {
this.verticalScrollState = new ScrollState();
this.horizontalScrollState = new ScrollState();
initializeScrollListener();
}
this.browsedPath = path;
threadService.runActionLater(() -> {
current.currentEditor().updatePreviewUrl();
this.treeView = controller.getFileSystemView();
rootItem = new PathItem(new Item(path, String.format("%s", Optional.of(path).map(Path::getFileName).orElse(path))), awesomeService.getIcon(path));
rootItem.getChildren().add(new PathItem(new Item(null, "Loading..")));
treeView.setRoot(rootItem);
rootItem.setExpanded(true);
this.addPathToTree(path, rootItem, null);
logger.info("File browser relisted for {}", path);
}, true);
}
private void initializeScrollListener() {
threadService.runActionLater(() -> {
this.treeView = controller.getFileSystemView();
Set<Node> nodes = this.treeView.lookupAll(".scroll-bar");
for (Node node : nodes) {
ScrollBar scrollBar = (ScrollBar) node;
if (scrollBar.getOrientation() == Orientation.VERTICAL) {
verticalScrollState.updateState(scrollBar);
scrollBar.valueProperty().addListener((observable, oldValue, newValue) -> {
verticalScrollState.updateState(scrollBar, newValue);
});
} else if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
horizontalScrollState.updateState(scrollBar);
scrollBar.valueProperty().addListener((observable, oldValue, newValue) -> {
horizontalScrollState.updateState(scrollBar, newValue);
});
}
}
});
}
public void addPathToTree(Path path, final TreeItem<Item> treeItem, Path changedPath) {
threadService.runTaskLater((() -> {
if (Objects.isNull(path) || Objects.isNull(treeItem)) {
return;
}
if (!Files.isDirectory(path)) {
return;
}
if (!Files.exists(path)) {
return;
}
if (treeItem == treeView.getRoot()) { // is root
pathItemMap.clear();
directoryItemMap.clear();
fileWatchService.reCreateWatchService();
}
directoryItemMap.put(path, treeItem);
pathItemMap.put(path, treeItem);
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path);) {
List<TreeItem<Item>> subItemList = StreamSupport
.stream(directoryStream.spliterator(), false)
.sorted(pathOrder::comparePaths)
.map(p -> {
TreeItem<Item> childItem = new PathItem(new Item(p), awesomeService.getIcon(p));
if (Files.isDirectory(p)) {
if (!IOHelper.isEmptyDir(p)) {
childItem.getChildren().add(new PathItem(new Item(null, "Loading..")));
}
childItem.setExpanded(false);
childItem.expandedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
addPathToTree(childItem.getValue().getPath(), childItem, null);
}
// fixes not expand issue
treeView.refresh();
});
}
pathItemMap.put(p, childItem);
return childItem;
})
.collect(Collectors.toList());
threadService.runActionLater(() -> {
saveTreeSelectionState();
boolean treeViewFocused = treeView.isFocused();
treeItem.getChildren().clear();
treeView.getSelectionModel().clearSelection();
treeItem.getChildren().addAll(subItemList);
restoreTreeSelectionState();
restoreTreeScrollState();
if (treeViewFocused) {
treeView.requestFocus();
}
if (Objects.nonNull(changedPath)) {
TreeItem<Item> item = pathItemMap.get(changedPath);
if (Objects.nonNull(item)) {
treeView.getSelectionModel().clearSelection();
treeView.getSelectionModel().select(item);
treeView.scrollTo(findIndex(item));
TreeItem<Item> parent = item.getParent();
if (Objects.nonNull(parent)) {
if (!parent.isExpanded()) {
parent.setExpanded(true);
}
}
}
}
fileWatchService.registerPathWatcher(path);
});
} catch (Exception e) {
logger.warn("Problem occured while updating file browser", e);
}
}));
}
private void restoreTreeScrollState() {
threadService.schedule(() -> { // run after some ms
threadService.runActionLater(() -> { // run in ui thread
Set<Node> nodes = this.treeView.lookupAll(".scroll-bar");
for (Node node : nodes) {
ScrollBar scrollBar = (ScrollBar) node;
if (scrollBar.getOrientation() == Orientation.VERTICAL) {
verticalScrollState.restoreState(scrollBar);
} else if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
horizontalScrollState.restoreState(scrollBar);
}
}
});
}, 50, TimeUnit.MILLISECONDS);
}
private void restoreTreeSelectionState() {
for (Path lastSelectedPath : lastSelectedItems) {
TreeItem<Item> item = pathItemMap.get(lastSelectedPath);
if (Objects.nonNull(item)) {
treeView.getSelectionModel().select(item);
}
}
}
private void saveTreeSelectionState() {
try {
lastSelectedItems = treeView.getSelectionModel()
.getSelectedItems()
.stream()
.map(TreeItem::getValue)
.filter(Objects::nonNull)
.map(Item::getPath)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
} catch (Exception ex) {
}
}
public void refreshPathToTree(Path path, Path changedPath) {
TreeItem<Item> item = directoryItemMap.get(path);
addPathToTree(path, item, changedPath);
}
TreeItem<Item> searchFoundItem;
public void searchUpAndSelect(String text) {
threadService.runTaskLater(() -> {
List<TreeItem<Item>> foundItems = searchItems(text);
if (foundItems.isEmpty()) {
return;
}
ListIterator<TreeItem<Item>> listIterator = foundItems.listIterator();
while (true) {
if (Objects.isNull(searchFoundItem)) {
if (listIterator.hasNext()) {
searchFoundItem = listIterator.next();
}
break;
}
if (listIterator.hasNext()) {
TreeItem<Item> next = listIterator.next();
if (next.getValue().equals(searchFoundItem.getValue())) {
if (listIterator.hasNext()) {
TreeItem<Item> nexted = listIterator.next();
if (next == nexted) {
if (listIterator.hasNext()) {
nexted = listIterator.next();
}
}
searchFoundItem = nexted;
break;
}
}
} else {
break;
}
}
focusFoundItem(searchFoundItem);
});
}
public void searchDownAndSelect(String text) {
threadService.runTaskLater(() -> {
List<TreeItem<Item>> foundItems = searchItems(text);
if (foundItems.isEmpty()) {
return;
}
ListIterator<TreeItem<Item>> listIterator = foundItems.listIterator();
while (true) {
if (Objects.isNull(searchFoundItem)) {
if (listIterator.hasPrevious()) {
searchFoundItem = listIterator.previous();
}
break;
}
if (listIterator.hasNext()) {
TreeItem<Item> next = listIterator.next();
if (next.getValue().equals(searchFoundItem.getValue())) {
if (listIterator.hasPrevious()) {
TreeItem<Item> previous = listIterator.previous();
if (next == previous) {
if (listIterator.hasPrevious()) {
previous = listIterator.previous();
}
}
searchFoundItem = previous;
break;
}
}
} else {
break;
}
}
focusFoundItem(searchFoundItem);
});
}
private void focusFoundItem(TreeItem<Item> searchFoundItem) {
if (Objects.nonNull(searchFoundItem)) {
TreeView<Item> fileSystemView = controller.getFileSystemView();
threadService.runActionLater(() -> {
fileSystemView.getSelectionModel().clearSelection();
int selectedIndex = findIndex(searchFoundItem);
fileSystemView.getSelectionModel().select(searchFoundItem);
fileSystemView.scrollTo(selectedIndex);
TreeItem<Item> parent = searchFoundItem.getParent();
if (Objects.nonNull(parent)) {
if (!parent.isExpanded()) {
parent.setExpanded(true);
}
}
}, true);
}
}
private List<TreeItem<Item>> searchItems(String text) {
PathMatcher pathMatcher = null;
try {
String syntaxAndPattern = String.format("glob:**%s**", text);
pathMatcher = FileSystems.getDefault().getPathMatcher(syntaxAndPattern);
} catch (PatternSyntaxException psex) {
return new ArrayList<>();
}
final PathMatcher finalPathMatcher = pathMatcher;
Optional.ofNullable(searchFoundItem)
.map(TreeItem::getValue)
.map(Item::getPath)
.filter(p -> !finalPathMatcher.matches(p))
.ifPresent(p -> searchFoundItem = null);
if (Objects.nonNull(searchFoundItem)) {
if (!pathItemMap.containsValue(searchFoundItem)) {
searchFoundItem = null;
}
}
return pathItemMap.values()
.stream()
.map(e -> Optional.ofNullable(e))
.filter(o -> o
.map(TreeItem::getValue)
.map(Item::getPath)
.filter(p -> !p.equals(p.getRoot()))
.filter(p -> finalPathMatcher.matches(p))
.isPresent())
.map(e -> e.get())
.sorted((p1, p2) -> pathOrder.comparePaths(p1.getValue().getPath(), p2.getValue().getPath()))
.collect(Collectors.toList());
}
public int findIndex(TreeItem<Item> changedItem) {
TreeItem<Item> item = changedItem;
int result = 0;
while (true) {
TreeItem<Item> parent = item.getParent();
if (Objects.isNull(parent)) {
break;
}
int index = parent.getChildren().indexOf(item);
result += index;
item = parent;
}
return result;
}
public void searchAndSelect(String text) {
threadService.runTaskLater(() -> {
List<TreeItem<Item>> foundItems = searchItems(text.trim());
if (foundItems.isEmpty()) {
return;
}
ListIterator<TreeItem<Item>> listIterator = foundItems.listIterator();
if (Objects.isNull(searchFoundItem)) {
if (listIterator.hasNext()) {
searchFoundItem = listIterator.next();
}
}
focusFoundItem(searchFoundItem);
});
}
}