diff --git a/project_view_pane/src/main/java/org/intellij/sdk/view/pane/ImagesProjectNode.java b/project_view_pane/src/main/java/org/intellij/sdk/view/pane/ImagesProjectNode.java index 8e7f2c88b..28729a3a2 100644 --- a/project_view_pane/src/main/java/org/intellij/sdk/view/pane/ImagesProjectNode.java +++ b/project_view_pane/src/main/java/org/intellij/sdk/view/pane/ImagesProjectNode.java @@ -1,72 +1,86 @@ -// Copyright 2000-2022 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. - +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.intellij.sdk.view.pane; import com.intellij.icons.AllIcons; import com.intellij.ide.projectView.PresentationData; import com.intellij.ide.projectView.ProjectView; +import com.intellij.ide.projectView.ProjectViewNode; +import com.intellij.ide.projectView.ViewSettings; +import com.intellij.ide.projectView.impl.GroupByTypeComparator; import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectUtil; -import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.roots.FileIndex; +import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.VirtualFileEvent; -import com.intellij.openapi.vfs.VirtualFileListener; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.newvfs.BulkFileListener; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; import com.intellij.psi.search.FilenameIndex; -import com.intellij.util.Alarm; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.ui.update.MergingUpdateQueue; +import com.intellij.util.ui.update.Update; import org.jetbrains.annotations.NotNull; -import javax.swing.*; import java.util.*; -public class ImagesProjectNode extends AbstractTreeNode { +public class ImagesProjectNode extends ProjectViewNode { private static final Key> IMAGES_PROJECT_DIRS = Key.create("images.files.or.directories"); - public ImagesProjectNode(final Project project) { - super(project, ProjectUtil.guessProjectDir(project)); + private static final List SUPPORTED_IMAGE_EXTENSIONS = List.of("jpg", "jpeg", "png", "svg"); + + private final MergingUpdateQueue updateQueue; + + /** + * Creates root node. + */ + public ImagesProjectNode(@NotNull Project project, + @NotNull ViewSettings settings, + @NotNull VirtualFile rootDir, + @NotNull Disposable parentDisposable) { + super(project, rootDir, settings); scanImages(project); - - subscribeToVFS(project); + setupImageFilesRefresher(project, parentDisposable); // subscribe to changes only in the root node + updateQueue = new MergingUpdateQueue(ImagesProjectNode.class.getName(), 200, true, null, parentDisposable, null); } - public ImagesProjectNode(Project project, VirtualFile file) { - super(project, file); + /** + * Creates child node. + */ + private ImagesProjectNode(@NotNull Project project, + @NotNull ViewSettings settings, + @NotNull VirtualFile file, + @NotNull MergingUpdateQueue updateQueue) { + super(project, file, settings); + this.updateQueue = updateQueue; } - private void scanImages(Project project) { - addAllByExt(project, "png"); - addAllByExt(project, "jpg"); - addAllByExt(project, "svg"); + private void scanImages(@NotNull Project project) { + for (String imageExtension : SUPPORTED_IMAGE_EXTENSIONS) { + addAllByExt(project, imageExtension); + } } - // Creates a collection of image files asynchronously - private void addAllByExt(Project project, String ext) { - final Set imagesFiles = getImagesFiles(project); - final VirtualFile projectDir = ProjectUtil.guessProjectDir(project); - - try { - final Collection files = ReadAction.compute(() -> FilenameIndex.getAllFilesByExt(project, ext)); - - for (VirtualFile file : files) { - while (file != null && !file.equals(projectDir)) { - imagesFiles.add(file); - file = file.getParent(); - } + private void addAllByExt(@NotNull Project project, @NotNull String extension) { + Set imagesFiles = getImagesFiles(project); + VirtualFile projectDir = ProjectUtil.guessProjectDir(project); + Collection files = ReadAction.compute(() -> FilenameIndex.getAllFilesByExt(project, extension)); + for (VirtualFile file : files) { + while (file != null && !file.equals(projectDir)) { + imagesFiles.add(file); + file = file.getParent(); } - - } catch (Throwable throwable) { - throwable.printStackTrace(); } } @NotNull - private Set getImagesFiles(Project project) { + private Set getImagesFiles(@NotNull Project project) { Set files = project.getUserData(IMAGES_PROJECT_DIRS); if (files == null) { files = new HashSet<>(); @@ -75,15 +89,57 @@ public class ImagesProjectNode extends AbstractTreeNode { return files; } + private void setupImageFilesRefresher(@NotNull Project project, @NotNull Disposable parentDisposable) { + project.getMessageBus().connect(parentDisposable) + .subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() { + @Override + public void after(@NotNull List events) { + boolean hasAnyImageUpdate = false; + FileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex(); + for (VFileEvent event : events) { + VirtualFile file = event.getFile(); + if (file == null || !fileIndex.isInContent(file)) { + continue; + } + String extension = file.getExtension(); + if (extension != null && SUPPORTED_IMAGE_EXTENSIONS.contains(extension)) { + hasAnyImageUpdate = true; + break; + } + } + if (hasAnyImageUpdate) { + updateQueue.queue(new Update("UpdateImages") { + public void run() { + getImagesFiles(project).clear(); + scanImages(project); + ApplicationManager.getApplication().invokeLater(() -> + ProjectView.getInstance(project) + .getProjectViewPaneById(ImagesProjectViewPane.ID) + .updateFromRoot(true), + project.getDisposed() + ); + } + }); + } + } + }); + } + @Override - protected VirtualFile getVirtualFile() { + public boolean contains(@NotNull VirtualFile file) { + return file.equals(getVirtualFile()); + } + + @Override + @NotNull + public VirtualFile getVirtualFile() { return getValue(); } @NotNull @Override public Collection> getChildren() { - final List files = new ArrayList<>(0); + List files = new ArrayList<>(); for (VirtualFile file : getValue().getChildren()) { if (getImagesFiles(myProject).contains(file)) { files.add(file); @@ -92,30 +148,14 @@ public class ImagesProjectNode extends AbstractTreeNode { if (files.isEmpty()) { return Collections.emptyList(); } - final List> nodes = new ArrayList<>(files.size()); - final boolean alwaysOnTop = ProjectView.getInstance(myProject).isFoldersAlwaysOnTop(""); - files.sort((o1, o2) -> { - if (alwaysOnTop) { - final boolean d1 = o1.isDirectory(); - final boolean d2 = o2.isDirectory(); - if (d1 && !d2) { - return -1; - } - if (!d1 && d2) { - return 1; - } - } - - return StringUtil.naturalCompare(o1.getName(), o2.getName()); - }); - for (VirtualFile file : files) { - nodes.add(new ImagesProjectNode(myProject, file)); - } - return nodes; + ViewSettings settings = getSettings(); + return ContainerUtil.sorted( + ContainerUtil.map(files, (file) -> new ImagesProjectNode(myProject, settings, file, updateQueue)), + new GroupByTypeComparator(myProject, ImagesProjectViewPane.ID)); } @Override - protected void update(PresentationData data) { + protected void update(@NotNull PresentationData data) { data.setIcon(getValue().isDirectory() ? AllIcons.Nodes.Folder : getValue().getFileType().getIcon()); data.setPresentableText(getValue().getName()); } @@ -135,38 +175,10 @@ public class ImagesProjectNode extends AbstractTreeNode { FileEditorManager.getInstance(myProject).openFile(getValue(), false); } - private void subscribeToVFS(final Project project) { - final Alarm alarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, project); - LocalFileSystem.getInstance().addVirtualFileListener(new VirtualFileListener() { - { - final VirtualFileListener me = this; - Disposer.register(project, () -> LocalFileSystem.getInstance().removeVirtualFileListener(me)); - } - - @Override - public void fileCreated(@NotNull VirtualFileEvent event) { - handle(event); - } - - @Override - public void fileDeleted(@NotNull VirtualFileEvent event) { - handle(event); - } - - void handle(VirtualFileEvent event) { - final String filename = event.getFileName().toLowerCase(); - if (filename.endsWith(".png") || filename.endsWith(".jpg")) { - alarm.cancelAllRequests(); - alarm.addRequest(() -> { - getImagesFiles(project).clear(); - scanImages(project); - SwingUtilities.invokeLater(() -> ProjectView.getInstance(myProject) - .getProjectViewPaneById(ImagesProjectViewPane.ID) - .updateFromRoot(true)); - }, 1000); - } - } - }); + @Override + public int getTypeSortWeight(boolean sortByType) { + // required for "Folder Always on Top" + return getVirtualFile().isDirectory() ? 1 : 0; } } diff --git a/project_view_pane/src/main/java/org/intellij/sdk/view/pane/ImagesProjectViewPane.java b/project_view_pane/src/main/java/org/intellij/sdk/view/pane/ImagesProjectViewPane.java index 0407e0340..203687dcc 100644 --- a/project_view_pane/src/main/java/org/intellij/sdk/view/pane/ImagesProjectViewPane.java +++ b/project_view_pane/src/main/java/org/intellij/sdk/view/pane/ImagesProjectViewPane.java @@ -7,11 +7,13 @@ import com.intellij.ide.impl.ProjectViewSelectInTarget; import com.intellij.ide.projectView.ViewSettings; import com.intellij.ide.projectView.impl.*; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; import javax.swing.tree.DefaultTreeModel; -public class ImagesProjectViewPane extends AbstractProjectViewPaneWithAsyncSupport { +public class ImagesProjectViewPane extends ProjectViewPane { public static final String ID = "IMAGES"; @@ -70,7 +72,16 @@ public class ImagesProjectViewPane extends AbstractProjectViewPaneWithAsyncSuppo return new ProjectTreeStructure(myProject, ID) { @Override protected ImagesProjectNode createRoot(@NotNull Project project, @NotNull ViewSettings settings) { - return new ImagesProjectNode(project); + return new ImagesProjectNode(project, settings, getProjectDir(project), ImagesProjectViewPane.this); + } + + @NotNull + private static VirtualFile getProjectDir(Project project) { + VirtualFile guessedProjectDir = ProjectUtil.guessProjectDir(project); + if (guessedProjectDir == null) { + throw new IllegalStateException("Could not get project directory"); + } + return guessedProjectDir; } // Children will be searched in async mode