package com.kodcu.service.ui;
import com.kodcu.component.*;
import com.kodcu.config.EditorConfigBean;
import com.kodcu.config.StoredConfigBean;
import com.kodcu.controller.ApplicationController;
import com.kodcu.other.Current;
import com.kodcu.other.ExtensionFilters;
import com.kodcu.other.IOHelper;
import com.kodcu.other.Item;
import com.kodcu.service.*;
import com.kodcu.service.extension.AsciiTreeGenerator;
import com.kodcu.service.shortcut.ShortcutProvider;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableObjectValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.stage.FileChooser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Created by usta on 25.12.2014.
*/
@Component
public class TabService {
private final Logger logger = LoggerFactory.getLogger(TabService.class);
private final ApplicationController controller;
private final EditorService editorService;
private final PathResolverService pathResolver;
private final ThreadService threadService;
private final Current current;
private final DirectoryService directoryService;
private final StoredConfigBean storedConfigBean;
private final ParserService parserService;
private final ApplicationContext applicationContext;
private final ShortcutProvider shortcutProvider;
private final AsciiTreeGenerator asciiTreeGenerator;
@Value("${application.editor.url}")
private String editorUrl;
@Value("${application.epub.url}")
private String epubUrl;
private ObservableList<Optional<Path>> closedPaths = FXCollections.observableArrayList();
@Autowired
public TabService(final ApplicationController controller, final EditorService editorService,
final PathResolverService pathResolver, final ThreadService threadService, final Current current,
final DirectoryService directoryService, StoredConfigBean storedConfigBean, ParserService parserService, ApplicationContext applicationContext, ShortcutProvider shortcutProvider, AsciiTreeGenerator asciiTreeGenerator) {
this.controller = controller;
this.editorService = editorService;
this.pathResolver = pathResolver;
this.threadService = threadService;
this.current = current;
this.directoryService = directoryService;
this.storedConfigBean = storedConfigBean;
this.parserService = parserService;
this.applicationContext = applicationContext;
this.shortcutProvider = shortcutProvider;
this.asciiTreeGenerator = asciiTreeGenerator;
}
public void addTab(Path path, Runnable... runnables) {
ObservableList<Item> recentFiles = storedConfigBean.getRecentFiles();
if (Files.notExists(path)) {
recentFiles.remove(path.toString());
logger.debug("Path {} not found in the filesystem", path);
return;
}
ObservableList<Tab> tabs = controller.getTabPane().getTabs();
for (Tab tab : tabs) {
if (tab instanceof MyTab) {
MyTab myTab = (MyTab) tab;
Path currentPath = myTab.getPath();
if (Objects.nonNull(currentPath))
if (currentPath.equals(path)) {
myTab.select(); // Select already added tab
return;
}
}
}
AnchorPane anchorPane = new AnchorPane();
MyTab tab = createTab();
tab.setTabText(path.getFileName().toString());
EditorPane editorPane = tab.getEditorPane();
threadService.runActionLater(() -> {
TabPane tabPane = controller.getTabPane();
tabPane.getTabs().add(tab);
tab.select();
});
Node editorVBox = editorService.createEditorVBox(editorPane, tab);
controller.fitToParent(editorVBox);
anchorPane.getChildren().add(editorVBox);
tab.setContent(anchorPane);
tab.setPath(path);
Tooltip tip = new Tooltip(path.toString());
Tooltip.install(tab.getGraphic(), tip);
recentFiles.remove(new Item(path));
recentFiles.add(0, new Item(path));
editorPane.getHandleReadyTasks().clear();
editorPane.getHandleReadyTasks().addAll(runnables);
editorPane.load(String.format(editorUrl, controller.getPort()));
}
public void newDoc() {
newDoc("");
}
public void newDoc(final String content) {
MyTab tab = this.createTab();
EditorPane editorPane = tab.getEditorPane();
editorPane.setInitialEditorValue(content);
AnchorPane anchorPane = new AnchorPane();
Node editorVBox = editorService.createEditorVBox(editorPane, tab);
controller.fitToParent(editorVBox);
anchorPane.getChildren().add(editorVBox);
tab.setContent(anchorPane);
tab.setTabText("new *");
TabPane tabPane = controller.getTabPane();
tabPane.getTabs().add(tab);
tab.select();
editorPane.load(String.format(editorUrl, controller.getPort()));
}
public void openDoc() {
FileChooser fileChooser = directoryService.newFileChooser("Open File");
fileChooser.getExtensionFilters().add(ExtensionFilters.ASCIIDOC);
fileChooser.getExtensionFilters().add(ExtensionFilters.MARKDOWN);
fileChooser.getExtensionFilters().add(ExtensionFilters.ALL);
List<File> chosenFiles = fileChooser.showOpenMultipleDialog(controller.getStage());
if (chosenFiles != null) {
chosenFiles.stream().map(File::toPath).forEach(this::previewDocument);
ObservableList<Item> recentFiles = storedConfigBean.getRecentFiles();
chosenFiles.stream()
.map(e -> new Item(e.toPath()))
.filter(file -> !recentFiles.contains(file)).forEach(recentFiles::addAll);
directoryService.setInitialDirectory(Optional.ofNullable(chosenFiles.get(0)));
}
}
public Path getSelectedTabPath() {
TreeItem<Item> selectedItem = controller.getFileSystemView().getSelectionModel().getSelectedItem();
Item value = selectedItem.getValue();
Path path = value.getPath();
return path;
}
// TODO: It is not a right place for this helper
public List<Path> getSelectedTabPaths() {
ObservableList<TreeItem<Item>> treeItems = controller.getFileSystemView().getSelectionModel().getSelectedItems();
return treeItems.stream()
.map(TreeItem::getValue)
.map(Item::getPath)
.collect(Collectors.toList());
}
public MyTab createTab() {
final MyTab tab = applicationContext.getBean(MyTab.class);
tab.setOnCloseRequest(event -> {
event.consume();
tab.close();
});
MenuItem menuItem0 = new MenuItem("Close");
menuItem0.setOnAction(actionEvent -> {
tab.close();
});
MenuItem menuItem1 = new MenuItem("Close All");
menuItem1.setOnAction(controller::closeAllTabs);
MenuItem menuItem2 = new MenuItem("Close Others");
menuItem2.setOnAction(event -> {
ObservableList<Tab> blackList = FXCollections.observableArrayList();
blackList.addAll(tab.getTabPane().getTabs());
blackList.remove(tab);
blackList.stream()
.filter(t -> t instanceof MyTab)
.map(t -> (MyTab) t).sorted((mo1, mo2) -> {
if (mo1.isNew() && !mo2.isNew())
return -1;
else if (mo2.isNew() && !mo1.isNew()) {
return 1;
}
return 0;
}).forEach(myTab -> {
if (event.isConsumed())
return;
ButtonType close = myTab.close();
if (close == ButtonType.CANCEL)
event.consume();
});
});
//
// MenuItem menuItem3 = new MenuItem("Close Unmodified");
// menuItem3.setOnAction(actionEvent -> {
//
// ObservableList<Tab> clonedTabs = FXCollections.observableArrayList();
// clonedTabs.addAll(controller.getTabPane().getTabs());
//
//
// for (Tab clonedTab : clonedTabs) {
// MyTab myTab = (MyTab) clonedTab;
// if (!myTab.getTabText().contains(" *"))
// threadService.runActionLater(()->{
// myTab.close();
// });
// }
// });
MenuItem menuItem4 = new MenuItem("Select Next Tab");
menuItem4.setOnAction(actionEvent -> {
TabPane tabPane = tab.getTabPane();
if (tabPane.getSelectionModel().isSelected(tabPane.getTabs().size() - 1))
tabPane.getSelectionModel().selectFirst();
else
tabPane.getSelectionModel().selectNext();
});
MenuItem menuItem5 = new MenuItem("Select Previous Tab");
menuItem5.setOnAction(actionEvent -> {
SingleSelectionModel<Tab> selectionModel = tab.getTabPane().getSelectionModel();
if (selectionModel.isSelected(0))
selectionModel.selectLast();
else
selectionModel.selectPrevious();
});
MenuItem menuItem6 = new MenuItem("Reopen Closed Tab");
menuItem6.setOnAction(actionEvent -> {
if (closedPaths.size() > 0) {
int index = closedPaths.size() - 1;
closedPaths.get(index).filter(pathResolver::isAsciidoc).ifPresent(this::addTab);
closedPaths.get(index).filter(pathResolver::isMarkdown).ifPresent(this::addTab);
closedPaths.get(index).filter(pathResolver::isImage).ifPresent(this::addImageTab);
closedPaths.remove(index);
}
});
MenuItem menuItem7 = new MenuItem("Browse");
menuItem7.setOnAction(event -> {
current.currentPath()
.map(Path::getParent)
.ifPresent(controller::openInDesktop);
});
MenuItem copyItem = MenuItemBuilt.item("Copy").click(event -> {
Optional.ofNullable(tab.getPath())
.ifPresent(path -> controller.copyFiles(Arrays.asList(path)));
});
MenuItem copyPathItem = MenuItemBuilt.item("Copy Path").click(event -> {
Optional.ofNullable(tab.getPath())
.map(Path::toString)
.ifPresent(controller::cutCopy);
});
MenuItem menuItem8 = new MenuItem("New File");
menuItem8.setOnAction(controller::newDoc);
MenuItem reloadMenuItem = new MenuItem("Reload");
reloadMenuItem.setOnAction(event -> {
tab.load();
});
MenuItem gotoWorkdir = new MenuItem("Go to Workdir");
gotoWorkdir.setOnAction(event -> {
current.currentPath().map(Path::getParent).ifPresent(directoryService::changeWorkigDir);
});
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().addAll(menuItem0, menuItem1, menuItem2, new SeparatorMenuItem(),
menuItem4, menuItem5, menuItem6, new SeparatorMenuItem(), reloadMenuItem,
new SeparatorMenuItem(), gotoWorkdir, new SeparatorMenuItem(),
menuItem7, copyItem, copyPathItem, menuItem8);
tab.contextMenuProperty().setValue(contextMenu);
Label label = tab.getLabel();
label.setOnMouseClicked(mouseEvent -> {
if (mouseEvent.getButton().equals(MouseButton.SECONDARY)) {
tab.select();
} else if (mouseEvent.getClickCount() > 1) {
controller.adjustSplitPane();
}
});
return tab;
}
public void previewDocument(Path path) {
if (Objects.isNull(path)) {
logger.error("Null path cannot be viewed");
return;
}
if (Files.isDirectory(path)) {
if (path.equals(directoryService.workingDirectory())) {
directoryService.changeWorkigDir(path.getParent());
} else {
directoryService.changeWorkigDir(path);
}
} else if (pathResolver.isImage(path)) {
addImageTab(path);
} else if (pathResolver.isEpub(path)) {
current.setCurrentEpubPath(path);
controller.browseInDesktop(String.format(epubUrl, controller.getPort()));
} else if (pathResolver.isPDF(path) || pathResolver.isArchive(path) || pathResolver.isVideo(path) || pathResolver.isOffice(path)) {
controller.openInDesktop(path);
} else {
Optional<Long> size = IOHelper.size(path);
int hangFileSizeLimit = controller.getHangFileSizeLimit();
if (size.isPresent()) {
if (size.get() > hangFileSizeLimit * 1024 * 1024) {
processHangAlert(AlertHelper.sizeHangAlert(path, hangFileSizeLimit), path);
} else {
addTab(path);
}
} else {
processHangAlert(AlertHelper.nosizeAlert(path, hangFileSizeLimit), path);
}
}
}
private void processHangAlert(Optional<ButtonType> buttonType, Path path) {
ButtonType btnType = buttonType.orElse(ButtonType.CANCEL);
if (btnType == AlertHelper.OPEN_IN_APP) {
controller.saveAllTabs();
addTab(path);
} else if (btnType == AlertHelper.OPEN_EXTERNAL) {
controller.openInDesktop(path);
}
}
public void addImageTab(Path imagePath) {
ImageTab tab = new ImageTab(imagePath);
final TabPane previewTabPane = controller.getTabPane();
if (previewTabPane.getTabs().contains(tab)) {
previewTabPane.getSelectionModel().select(tab);
return;
}
Image image = new Image(IOHelper.pathToUrl(imagePath));
ImageView imageView = new ImageView(image);
imageView.setPreserveRatio(true);
imageView.setFitWidth(previewTabPane.getWidth());
previewTabPane.widthProperty().addListener((observable, oldValue, newValue) -> {
imageView.setFitWidth(previewTabPane.getWidth());
});
Tooltip tip = new Tooltip(imagePath.toString());
Tooltip.install(tab.getGraphic(), tip);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setContent(imageView);
scrollPane.addEventFilter(ScrollEvent.SCROLL, e -> {
if (e.isShortcutDown() && e.getDeltaY() > 0) {
// zoom in
imageView.setFitWidth(imageView.getFitWidth() + 16.0);
} else if (e.isControlDown() && e.getDeltaY() < 0) {
// zoom out
imageView.setFitWidth(imageView.getFitWidth() - 16.0);
}
});
tab.setContent(scrollPane);
previewTabPane.getTabs().add(tab);
previewTabPane.getSelectionModel().select(tab);
}
public void initializeTabChangeListener(TabPane tabPane) {
ReadOnlyObjectProperty<Tab> itemProperty = tabPane.getSelectionModel().selectedItemProperty();
tabPane.setOnMouseReleased(event -> {
Optional.ofNullable(itemProperty)
.map(ObservableObjectValue::get)
.filter(e -> e instanceof MyTab)
.map(e -> (MyTab) e)
.map(MyTab::getEditorPane)
.ifPresent(EditorPane::focus);
});
itemProperty.addListener((observable, oldValue, selectedTab) -> {
Optional.ofNullable(selectedTab)
.filter(e -> e instanceof MyTab)
.map(e -> (MyTab) e)
.map(MyTab::getEditorPane)
.filter(EditorPane::getReady)
.ifPresent(EditorPane::updatePreviewUrl);
});
}
public ObservableList<Optional<Path>> getClosedPaths() {
return closedPaths;
}
public void applyForEachMyTab(Consumer<MyTab> consumer, List<? extends Tab> tabs) {
for (Tab tab : tabs) {
if (tab instanceof MyTab) {
MyTab myTab = (MyTab) tab;
consumer.accept(myTab);
}
}
}
public void applyForEachMyTab(Consumer<MyTab> consumer) {
ObservableList<Tab> tabs = controller.getTabPane().getTabs();
applyForEachMyTab(consumer, tabs);
}
}