diff --git a/gse-app/src/main/java/com/powsybl/gse/app/ProjectPane.java b/gse-app/src/main/java/com/powsybl/gse/app/ProjectPane.java index deef6af4..dd943d9a 100644 --- a/gse-app/src/main/java/com/powsybl/gse/app/ProjectPane.java +++ b/gse-app/src/main/java/com/powsybl/gse/app/ProjectPane.java @@ -16,21 +16,35 @@ import com.sun.javafx.stage.StageHelper; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.Event; +import javafx.geometry.Insets; import javafx.geometry.Orientation; +import javafx.geometry.Side; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.*; +import javafx.scene.control.cell.TreeItemPropertyValueFactory; import javafx.scene.image.ImageView; import javafx.scene.input.*; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; +import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.util.Callback; +import org.controlsfx.control.CheckComboBox; +import org.controlsfx.control.MasterDetailPane; +import org.controlsfx.control.textfield.CustomTextField; +import org.controlsfx.control.textfield.TextFields; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,6 +52,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Geoffroy Jamgotchian @@ -48,6 +63,8 @@ public class ProjectPane extends Tab { private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle("lang.ProjectPane"); + private static final ServiceLoaderCache FILE_EXTENSION_LOADER = new ServiceLoaderCache<>(ProjectFileExtension.class); + private static final ServiceLoaderCache CREATOR_EXTENSION_LOADER = new ServiceLoaderCache<>(ProjectFileCreatorExtension.class); private static final ServiceLoaderCache EDITOR_EXTENSION_LOADER = new ServiceLoaderCache<>(ProjectFileEditorExtension.class); @@ -58,6 +75,10 @@ public class ProjectPane extends Tab { private final KeyCombination saveKeyCombination = new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN); + private final CustomTextField dependencySearchField = (CustomTextField) TextFields.createClearableTextField(); + + private final CheckComboBox filterTypeBox; + private static class TabKey { private final String nodeId; @@ -158,6 +179,8 @@ private static void closeTab(Event event, MyTab tab) { private final TreeView treeView; + private final TreeTableView dependencyTableView; + private final StackPane viewPane; private final TaskItemList taskItems; @@ -231,6 +254,90 @@ private void treeViewChangeListener(ListChangeListener.Change> c) { + if (c.getList().isEmpty()) { + dependencyTableView.setContextMenu(null); + } else if (c.getList().size() == 1) { + TreeItem selectedTreeItem = c.getList().get(0); + Object value = selectedTreeItem.getValue(); + dependencyTableView.setOnKeyPressed((KeyEvent ke) -> { + if (ke.getCode() == KeyCode.F2) { + renameProjectNode(selectedTreeItem); + } + }); + if (value instanceof ProjectFile) { + dependencyTableView.setContextMenu(createFileContextMenu(selectedTreeItem)); + } else { + dependencyTableView.setContextMenu(null); + } + } else { + dependencyTableView.setContextMenu(createMultipleContextMenu(c.getList())); + } + } + + private ChangeListener dependencyTextFieldListener() { + return (observable, oldValue, newValue) -> { + TreeItem rootItem = createDependencyRootItem(); + dependencyTableView.setRoot(rootItem); + List> filteredItems; + + ObservableList checkedTypes = filterTypeBox.getCheckModel().getCheckedItems(); + Stream> sortedItems = dependencyTableView.getRoot().getChildren().stream() + .sorted(Comparator.comparing(fileItem -> fileItem.getValue().toString())); + + if (!newValue.isEmpty()) { + filteredItems = sortedItems + .filter(item -> isItemChecked(checkedTypes, item) && itemNameStartsWith(item, newValue)) + .collect(Collectors.toList()); + } else { + filteredItems = sortedItems + .filter(file -> isItemChecked(checkedTypes, file)) + .collect(Collectors.toList()); + } + dependencyTableView.getRoot().getChildren().setAll(filteredItems); + }; + } + + private void dependencyFilteredBoxListener(ListChangeListener.Change c) { + TreeItem rootItem = createDependencyRootItem(); + dependencyTableView.setRoot(rootItem); + List> filteredItems; + + ObservableList checkedTypes = c.getList(); + Stream> sortedItems = dependencyTableView.getRoot().getChildren().stream() + .sorted(Comparator.comparing(fileItem -> fileItem.getValue().toString())); + String searchText = dependencySearchField.getText(); + + String allTypes = RESOURCE_BUNDLE.getString("All"); + if (c.getList().size() == 1) { + String checkedType = c.getList().get(0); + dependencyTableView.setRoot(rootItem); + if (checkedType.equals(allTypes)) { + dependencyTableView.setRoot(rootItem); + } else { + filteredItems = sortedItems + .filter(file -> isItemChecked(checkedTypes, file)) + .collect(Collectors.toList()); + if (!searchText.isEmpty()) { + filteredItems = filteredItems.stream() + .filter(item -> itemNameStartsWith(item, searchText)) + .collect(Collectors.toList()); + } + dependencyTableView.getRoot().getChildren().setAll(filteredItems); + } + } else { + filteredItems = sortedItems + .filter(file -> isItemChecked(checkedTypes, file)) + .collect(Collectors.toList()); + if (!searchText.isEmpty()) { + filteredItems = filteredItems.stream() + .filter(item -> itemNameStartsWith(item, searchText)) + .collect(Collectors.toList()); + } + dependencyTableView.getRoot().getChildren().setAll(filteredItems); + } + } + private TreeCell treeViewCellFactory(TreeView item) { return new TreeCell() { @@ -308,6 +415,15 @@ private void treeViewMouseClickHandler(MouseEvent mouseEvent) { } } + private void dependencyViewMouseClickHandler(MouseEvent mouseEvent) { + if (mouseEvent.getClickCount() == 2) { + TreeItem selectedTreeItem = dependencyTableView.getSelectionModel().getSelectedItem(); + if (selectedTreeItem != null) { + runDefaultActionAfterDoubleClick(selectedTreeItem); + } + } + } + private void setDragOverStyle(TreeCell treeCell) { treeCell.getStyleClass().add("treecell-drag-over"); } @@ -451,6 +567,34 @@ public ProjectPane(Scene scene, Project project, GseContext context) { treeView.setCellFactory(this::treeViewCellFactory); treeView.setOnMouseClicked(this::treeViewMouseClickHandler); + dependencyTableView = new TreeTableView<>(); + dependencyTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + dependencyTableView.getSelectionModel().getSelectedItems().addListener(this::dependencyViewChangeListener); + dependencyTableView.setOnMouseClicked(this::dependencyViewMouseClickHandler); + + TreeTableColumn fileColumn = new TreeTableColumn<>(RESOURCE_BUNDLE.getString("Name")); + fileColumn.setPrefWidth(150); + TreeTableColumn locationColumn = new TreeTableColumn<>(RESOURCE_BUNDLE.getString("Location")); + TreeTableColumn referenceColumn = new TreeTableColumn<>(RESOURCE_BUNDLE.getString("Reference")); + TreeTableColumn creationDateColumn = new TreeTableColumn<>(RESOURCE_BUNDLE.getString("Creation")); + TreeTableColumn modificationDateColumn = new TreeTableColumn<>(RESOURCE_BUNDLE.getString("Modification")); + + fileColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("Name")); + locationColumn.setCellValueFactory(this::locationColumnCellValueFactory); + referenceColumn.setCellValueFactory(this::referenceColumnCellValueFactory); + creationDateColumn.setCellValueFactory(this::creationDateColumnCellValueFactory); + modificationDateColumn.setCellValueFactory(this::modificationDateColumCellValueFactory); + + dependencyTableView.getColumns().addAll(fileColumn, locationColumn, referenceColumn, creationDateColumn, modificationDateColumn); + + ObservableList filteredList = FXCollections.observableArrayList(RESOURCE_BUNDLE.getString("All")); + ObservableList types = createFilterTypes(); + filteredList.addAll(types); + filterTypeBox = new CheckComboBox<>(filteredList); + filterTypeBox.setMaxWidth(250); + filterTypeBox.getCheckModel().check(0); + filterTypeBox.getCheckModel().getCheckedItems().addListener(this::dependencyFilteredBoxListener); + DetachableTabPane ctrlTabPane1 = new DetachableTabPane(); DetachableTabPane ctrlTabPane2 = new DetachableTabPane(); ctrlTabPane1.setScope("Control"); @@ -459,7 +603,24 @@ public ProjectPane(Scene scene, Project project, GseContext context) { projectTab.setClosable(false); Tab taskTab = new Tab(RESOURCE_BUNDLE.getString("Tasks"), taskMonitorPane); taskTab.setClosable(false); - ctrlTabPane1.getTabs().add(projectTab); + + Text searchGlyph = Glyph.createAwesomeFont('\uf002').size("1.2em"); + dependencySearchField.setLeft(searchGlyph); + dependencySearchField.setPrefWidth(260); + dependencySearchField.getStyleClass().add("search-field"); + + HBox filterBox = new HBox(dependencySearchField, filterTypeBox); + HBox.setMargin(dependencySearchField, new Insets(0, 0, 0, 3)); + + MasterDetailPane masterDetailPane = new MasterDetailPane(); + masterDetailPane.setDetailSide(Side.TOP); + masterDetailPane.setDetailNode(filterBox); + masterDetailPane.setMasterNode(dependencyTableView); + masterDetailPane.setDividerPosition(0.06); + + Tab dependencyTab = new Tab(RESOURCE_BUNDLE.getString("Dependencies"), masterDetailPane); + dependencyTab.setClosable(false); + ctrlTabPane1.getTabs().addAll(projectTab, dependencyTab); ctrlTabPane2.getTabs().add(taskTab); SplitPane ctrlSplitPane = new SplitPane(ctrlTabPane1, ctrlTabPane2); ctrlSplitPane.setOrientation(Orientation.VERTICAL); @@ -492,6 +653,9 @@ public ProjectPane(Scene scene, Project project, GseContext context) { setContent(splitPane); createRootFolderTreeItem(project); + createDependencyRootFolderTreeItem(); + + dependencySearchField.textProperty().addListener(dependencyTextFieldListener()); getContent().setOnKeyPressed((KeyEvent ke) -> { if (saveKeyCombination.match(ke)) { @@ -507,6 +671,72 @@ public ProjectPane(Scene scene, Project project, GseContext context) { }); } + private ObservableValue modificationDateColumCellValueFactory(TreeTableColumn.CellDataFeatures column) { + TreeItem item = column.getValue(); + if (item != null && item.getValue() != null) { + return new SimpleStringProperty(item.getValue().getModificationDate().toLocalDate().toString()); + } + return new SimpleStringProperty(); + } + + private ObservableValue creationDateColumnCellValueFactory(TreeTableColumn.CellDataFeatures column) { + TreeItem item = column.getValue(); + if (item != null && item.getValue() != null) { + return new SimpleStringProperty(item.getValue().getCreationDate().toLocalDate().toString()); + } + return new SimpleStringProperty(); + } + + private ObservableValue referenceColumnCellValueFactory(TreeTableColumn.CellDataFeatures column) { + TreeItem item = column.getValue(); + if (item != null && item.getValue() != null && item != dependencyTableView.getRoot() && item.getParent() != dependencyTableView.getRoot()) { + List> dependencies = ((ProjectFile) item.getValue()).getDependencies(); + boolean isABackwardDependency = dependencies.stream().anyMatch(dep -> dep.getProjectNode().getId().equals(item.getParent().getValue().getId())); + if (isABackwardDependency) { + return new SimpleStringProperty(RESOURCE_BUNDLE.getString("ReferencedBy")); + } + return new SimpleStringProperty(RESOURCE_BUNDLE.getString("Reference")); + } else { + return new SimpleStringProperty(); + } + } + + private ObservableValue locationColumnCellValueFactory(TreeTableColumn.CellDataFeatures column) { + TreeItem item = column.getValue(); + if (item != null && item.getValue() != null) { + return new SimpleStringProperty("/" + item.getValue().getPath().toString().replace(item.getValue().getName(), "")); + } else { + return new SimpleStringProperty(""); + } + } + + private static boolean isItemChecked(ObservableList checkedTypes, TreeItem file) { + final String allTypes = RESOURCE_BUNDLE.getString("All"); + for (ProjectFileExtension fileExtension : findFileExtension(((ProjectFile) file.getValue()).getClass())) { + if (fileExtension != null) { + return checkedTypes.contains(fileExtension.getProjectFileTrivialName()) || checkedTypes.contains(allTypes); + } + } + return false; + } + + private static boolean itemNameStartsWith(TreeItem item, String value) { + return item.getValue().getName().toLowerCase().startsWith(value.toLowerCase()); + } + + private static ObservableList createFilterTypes() { + ObservableList types = FXCollections.observableArrayList(); + for (ProjectFileExtension fileExtension : FILE_EXTENSION_LOADER.getServices()) { + if (fileExtension != null) { + String fileEName = fileExtension.getProjectFileTrivialName(); + if (!types.contains(fileEName)) { + types.add(fileEName); + } + } + } + return types; + } + public Project getProject() { return project; } @@ -536,6 +766,13 @@ private void refresh(TreeItem item) { item.setExpanded(false); item.setExpanded(true); } + refreshDependencyView(); + } + + private void refreshDependencyView() { + //refresh dependency tree view + dependencyTableView.getRoot().setExpanded(false); + dependencyTableView.getRoot().setExpanded(true); } private TreeItem createNodeTreeItem(ProjectNode node) { @@ -554,6 +791,50 @@ private void createRootFolderTreeItem(Project project) { }); } + private void createDependencyRootFolderTreeItem() { + dependencyTableView.setRoot(new TreeItem<>()); + GseUtil.execute(context.getExecutor(), () -> { + TreeItem root2 = createDependencyRootItem(); + Platform.runLater(() -> { + dependencyTableView.setRoot(root2); + root2.setExpanded(true); + dependencyTableView.setShowRoot(false); + }); + }); + } + + private TreeItem createDependencyRootItem() { + TreeItem rootItem = new TreeItem<>(project.getRootFolder(), NodeGraphics.getGraphic(project.getRootFolder())); + rootItem.expandedProperty().addListener((observable, oldvalue, newvalue) -> { + if (newvalue) { + List> fileTreeItems = new ArrayList<>(); + addFilesTreeItems(project.getRootFolder(), fileTreeItems); + List> fileSortedItems = fileTreeItems.stream().sorted(Comparator.comparing(fileItem -> fileItem.getValue().toString())) + .collect(Collectors.toList()); + rootItem.getChildren().setAll(fileSortedItems); + } + }); + return rootItem; + } + + private void addFilesTreeItems(ProjectFolder folder, List> fileTreeItems) { + List allNodes = folder.getChildren(); + allNodes.forEach(projectNode -> { + if (projectNode instanceof ProjectFile) { + TreeItem fileTreeItem = createFileTreeItem((ProjectFile) projectNode); + fileTreeItems.add(fileTreeItem); + if (!projectNode.getBackwardDependencies().isEmpty()) { + projectNode.getBackwardDependencies().forEach(file -> fileTreeItem.getChildren().add(createFileTreeItem(file))); + } + if (!((ProjectFile) projectNode).getDependencies().isEmpty()) { + ((ProjectFile) projectNode).getDependencies().forEach(file -> fileTreeItem.getChildren().add(createFileTreeItem((ProjectFile) file.getProjectNode()))); + } + } else { + addFilesTreeItems((ProjectFolder) projectNode, fileTreeItems); + } + }); + } + private TreeItem createFolderTreeItem(ProjectFolder folder) { TreeItem item = new TreeItem<>(folder, NodeGraphics.getGraphic(folder)); item.setExpanded(false); @@ -607,6 +888,12 @@ private static TreeItem createTaskTreeItem(String taskPreviewName) { // extension search + private static List findFileExtension(Class type) { + return FILE_EXTENSION_LOADER.getServices().stream() + .filter(extension -> extension.getProjectFileClass().isAssignableFrom(type)) + .collect(Collectors.toList()); + } + private static List findCreatorExtension(Class type) { return CREATOR_EXTENSION_LOADER.getServices().stream() .filter(extension -> extension.getProjectFileType().isAssignableFrom(type)) @@ -636,16 +923,16 @@ private static List findExecutionTaskExtensio // contextual menu - private MenuItem createDeleteProjectNodeItem(List> selectedTreeItems) { + private MenuItem createDeleteProjectNodeItem(List selectedTreeItems) { MenuItem deleteMenuItem = new MenuItem(RESOURCE_BUNDLE.getString("Delete"), Glyph.createAwesomeFont('\uf1f8').size("1.1em")); deleteMenuItem.setOnAction(event -> deleteNodesAlert(selectedTreeItems)); deleteMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.DELETE)); - List> selectedItems = new ArrayList<>(selectedTreeItems); + List selectedItems = new ArrayList<>(selectedTreeItems); deleteMenuItem.setDisable(ancestorsExistIn(selectedItems) || selectedItems.contains(treeView.getRoot())); return deleteMenuItem; } - private void deleteNodesAlert(List> selectedTreeItems) { + private void deleteNodesAlert(List selectedTreeItems) { GseAlerts.deleteNodesAlert(selectedTreeItems).showAndWait().ifPresent(buttonType -> { if (buttonType == ButtonType.OK) { List> parentTreeItems = new ArrayList<>(); @@ -666,9 +953,9 @@ private void deleteNodesAlert(List> selectedTreeItems }); } - private boolean ancestorsExistIn(List> treeItems) { + private boolean ancestorsExistIn(List treeItems) { boolean found = false; - for (TreeItem treeItem : treeItems) { + for (TreeItem treeItem : treeItems) { if (treeItem != treeView.getRoot()) { AbstractNodeBase value = (AbstractNodeBase) treeItem.getValue(); found = treeItems.stream().filter(it -> it != treeItem).anyMatch(item -> ((AbstractNodeBase) item.getValue()).isAncestorOf(value)); @@ -705,6 +992,7 @@ private void renameProjectNode(TreeItem selectedTreeItem) { // to force the refresh selectedTreeItem.setValue(null); selectedTreeItem.setValue(selectedProjectNode); + refreshDependencyView(); // refresh impacted tabs Map> treeItemsToRefresh = new HashMap<>(); @@ -897,7 +1185,7 @@ private MenuItem createCreateFolderItem(TreeItem selectedTreeItem, Proje return menuItem; } - private ContextMenu createMultipleContextMenu(List> selectedTreeItems) { + private ContextMenu createMultipleContextMenu(List selectedTreeItems) { ContextMenu contextMenu = new ContextMenu(); contextMenu.getItems().add(createDeleteProjectNodeItem(selectedTreeItems)); return contextMenu; diff --git a/gse-app/src/main/resources/lang/ProjectPane.properties b/gse-app/src/main/resources/lang/ProjectPane.properties index 15e99614..98192d72 100644 --- a/gse-app/src/main/resources/lang/ProjectPane.properties +++ b/gse-app/src/main/resources/lang/ProjectPane.properties @@ -1,16 +1,23 @@ +All=All Close=Close CloseAll=Close All ConfirmationDialog=Confirmation Dialog CreateFolder=Create folder +Creation=Creation Data=Data Delete=Delete +Dependencies=Dependencies DoYouConfirm=Do you confirm? DragError=Drag error Error=Error FileWillBeDeleted=File %s will be deleted FilesWillBeDeleted=Files %s will be deleted +Location=Location +Modification=Modification Name=Name NewName=New Name Open=Open Rename=Rename +Reference=Reference +ReferencedBy=Referenced By Tasks=Tasks diff --git a/gse-app/src/main/resources/lang/ProjectPane_fr.properties b/gse-app/src/main/resources/lang/ProjectPane_fr.properties index a229b05e..ab998834 100644 --- a/gse-app/src/main/resources/lang/ProjectPane_fr.properties +++ b/gse-app/src/main/resources/lang/ProjectPane_fr.properties @@ -1,16 +1,23 @@ +All=Tout Close=Fermer CloseAll=Fermer Tout ConfirmationDialog=Fenêtre de confirmation CreateFolder=Créer dossier +Creation=Création Data=Données Delete=Supprimer +Dependencies=Dépendances DoYouConfirm=Confirmez-vous? DragError=Erreur de déplacement Error=Erreur FileWillBeDeleted=Le fichier %s sera supprimé FilesWillBeDeleted=Les fichiers %s seront supprimés +Location=Emplacement +Modification=Modification Name=Nom NewName=Entrez un nouveau nom Open=Ouvrir Rename=Renommer +Reference=Référence +ReferencedBy=Référencé par Tasks=Tâches