This is an automated email from the ASF dual-hosted git repository.

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new e51351440c Fix file explorer refresh collapsing tree and add scoped 
folder refresh (#7426)
e51351440c is described below

commit e51351440c440222665621077159e45d88a18963
Author: Lance <[email protected]>
AuthorDate: Sun Jul 5 05:56:13 2026 +0800

    Fix file explorer refresh collapsing tree and add scoped folder refresh 
(#7426)
    
    Signed-off-by: lance <[email protected]>
---
 .../org/apache/hop/ui/core/widget/TreeMemory.java  |   5 +
 .../perspective/explorer/ExplorerPerspective.java  | 346 +++++++++++++++++++--
 2 files changed, 325 insertions(+), 26 deletions(-)

diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
index ac87f3a944..7698b814cd 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
@@ -124,6 +124,11 @@ public class TreeMemory {
     map.clear();
   }
 
+  /** Remove all remembered expand/collapse state for one tree. */
+  public void clearTree(String treeName) {
+    map.entrySet().removeIf(entry -> entry.getKey().treeName.equals(treeName));
+  }
+
   /**
    * This method creates, adds and returns a tree listener that will keep 
track of the
    * expanded/collapsed state of the TreeItems. This state will then be stored 
in the TreeMemory
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 5e1f94fb9d..de696b85da 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -33,6 +33,7 @@ import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 import lombok.Getter;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Strings;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.commons.vfs2.FileSystemException;
 import org.apache.commons.vfs2.Selectors;
@@ -72,6 +73,7 @@ import 
org.apache.hop.metadata.refactor.MetadataReferenceFinder;
 import org.apache.hop.metadata.refactor.MetadataReferenceResult;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.engine.IPipelineEngine;
+import org.apache.hop.ui.core.ConstUi;
 import org.apache.hop.ui.core.FormDataBuilder;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.bus.HopGuiEvents;
@@ -262,6 +264,9 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
   private SearchMatcher filterMatcher = new SearchMatcher("", false, false, 
true);
   private Map<String, Boolean> treeStateBeforeFilter = null;
 
+  /** Last folder the user selected in the tree; used for scoped refresh. */
+  private String lastSelectedFolderPath;
+
   public ExplorerPerspective() {
     instance = this;
 
@@ -489,8 +494,11 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
           e);
     }
 
-    if (!StringUtils.equals(oldRootFolder, rootFolder)
-        || !StringUtils.equals(oldRootName, rootName)) {
+    if (!Strings.CS.equals(oldRootFolder, rootFolder)
+        || !Strings.CS.equals(oldRootName, rootName)) {
+      // Project or root folder changed — drop any folder path from the 
previous tree.
+      lastSelectedFolderPath = null;
+      TreeMemory.getInstance().clearTree(FILE_EXPLORER_TREE);
       // call the root changed listeners...
       //
       for (IExplorerRootChangedListener listener : rootChangedListeners) {
@@ -963,13 +971,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
           // User confirmed via the reference dialog — proceed directly to 
deletion.
           List<String> filenames = getRecursiveFilenames(fileObject, new 
ArrayList<>());
           fileObject.deleteAll();
-          treeItem.dispose();
-          for (String filename : filenames) {
-            TabItemHandler handler = findTabItemHandler(filename);
-            if (handler != null) {
-              removeTabItem(handler);
-            }
-          }
+          removeTreeItemAfterDelete(treeItem, filenames);
           return;
         }
       } else {
@@ -989,13 +991,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
           // User confirmed — proceed directly to deletion.
           List<String> allFilenames = getRecursiveFilenames(fileObject, new 
ArrayList<>());
           fileObject.deleteAll();
-          treeItem.dispose();
-          for (String filename : allFilenames) {
-            TabItemHandler handler = findTabItemHandler(filename);
-            if (handler != null) {
-              removeTabItem(handler);
-            }
-          }
+          removeTreeItemAfterDelete(treeItem, allFilenames);
           return;
         }
       }
@@ -1017,13 +1013,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
       if ((box.open() & SWT.YES) != 0) {
         List<String> filenames = getRecursiveFilenames(fileObject, new 
ArrayList<>());
         if (fileObject.deleteAll() > 0) {
-          treeItem.dispose();
-          for (String filename : filenames) {
-            TabItemHandler handler = findTabItemHandler(filename);
-            if (handler != null) {
-              removeTabItem(handler);
-            }
-          }
+          removeTreeItemAfterDelete(treeItem, filenames);
         }
       }
     } catch (Exception e) {
@@ -2837,14 +2827,220 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
   @Override
   public void clearSearchFilters() {
     if (searchText != null && !searchText.isDisposed()) {
-      searchText.setText("");
+      if (Utils.isEmpty(searchText.getText())) {
+        refresh();
+      } else {
+        searchText.setText("");
+      }
     } else {
       filterText = "";
       filterMatcher = new SearchMatcher("", false, false, true);
+      refresh();
     }
   }
 
   public void refresh() {
+    determineRootFolderName(hopGui);
+    if (Utils.isEmpty(filterText)
+        && !Utils.isEmpty(lastSelectedFolderPath)
+        && isUnderCurrentRoot(lastSelectedFolderPath)) {
+      refreshSelectedFolder();
+      return;
+    }
+    refreshEntireTree();
+  }
+
+  /** True when {@code path} is the explorer root or a folder inside it. */
+  private boolean isUnderCurrentRoot(String path) {
+    if (Utils.isEmpty(path) || Utils.isEmpty(rootFolder)) {
+      return false;
+    }
+    if (path.equals(rootFolder)) {
+      return true;
+    }
+    return isDescendant(rootFolder, path);
+  }
+
+  /** Refresh only the last selected folder, keeping the rest of the tree 
intact. */
+  private void refreshSelectedFolder() {
+    String folderPath = lastSelectedFolderPath;
+    if (Utils.isEmpty(folderPath)
+        || !isUnderCurrentRoot(folderPath)
+        || !isAccessibleFolder(folderPath)) {
+      fallbackRefreshFromParent(folderPath);
+      return;
+    }
+
+    try {
+      determineRootFolderName(hopGui);
+
+      for (IExplorerRefreshListener listener : refreshListeners) {
+        listener.beforeRefresh();
+      }
+
+      TreeItem folderItem = locateTreeItem(folderPath);
+      if (folderItem == null || folderItem.isDisposed()) {
+        fallbackRefreshFromParent(folderPath);
+        return;
+      }
+
+      TreeItemFolder tif = (TreeItemFolder) folderItem.getData();
+      if (tif == null || !tif.folder) {
+        fallbackRefreshFromParent(folderPath);
+        return;
+      }
+
+      tree.setRedraw(false);
+      try {
+        refreshTreeItemInPlace(folderItem);
+        tree.setSelection(folderItem);
+        tree.showItem(folderItem);
+      } finally {
+        tree.setRedraw(true);
+      }
+    } catch (Exception e) {
+      new ErrorDialog(
+          getShell(),
+          BaseMessages.getString(PKG, 
"ExplorerPerspective.Error.TreeRefresh.Header"),
+          BaseMessages.getString(PKG, 
"ExplorerPerspective.Error.TreeRefresh.Message"),
+          e);
+    }
+    updateSelection();
+  }
+
+  /**
+   * When the remembered folder no longer exists, remove any stale tree node 
and refresh its parent
+   * instead of rebuilding the whole tree.
+   */
+  private void fallbackRefreshFromParent(String folderPath) {
+    removeStaleTreeItem(folderPath);
+    lastSelectedFolderPath = getParentFolderPath(folderPath);
+
+    if (!Utils.isEmpty(lastSelectedFolderPath)
+        && isUnderCurrentRoot(lastSelectedFolderPath)
+        && isAccessibleFolder(lastSelectedFolderPath)) {
+      refreshSelectedFolder();
+    } else {
+      lastSelectedFolderPath = null;
+      refreshEntireTree();
+    }
+  }
+
+  private void refreshTreeItemInPlace(TreeItem folderItem) {
+    TreeItemFolder tif = (TreeItemFolder) folderItem.getData();
+    if (tif == null || !tif.folder) {
+      return;
+    }
+
+    int childDepth = tif.path.equals(rootFolder) ? 0 : tif.depth + 1;
+    refreshFolder(folderItem, tif.path, childDepth);
+    tif.loaded = true;
+    restoreTreeItemExpandedFromMemory(folderItem);
+  }
+
+  /**
+   * Removes the deleted tree item, refreshes its parent, closes related tabs, 
and updates the tree
+   * selection.
+   *
+   * @param treeItem the deleted tree item
+   * @param filenames the file paths associated with the deleted item
+   */
+  private void removeTreeItemAfterDelete(TreeItem treeItem, List<String> 
filenames) {
+    updateLastSelectedFolderAfterDelete(treeItem);
+    TreeItem parentItem = treeItem.getParentItem();
+    treeItem.dispose();
+    for (String filename : filenames) {
+      TabItemHandler handler = findTabItemHandler(filename);
+      if (handler != null) {
+        removeTabItem(handler);
+      }
+    }
+    if (parentItem != null && !parentItem.isDisposed()) {
+      refreshTreeItemInPlace(parentItem);
+      tree.setSelection(parentItem);
+      tree.showItem(parentItem);
+    }
+    updateSelection();
+  }
+
+  /**
+   * Updates the last selected folder path after a tree item is deleted.
+   *
+   * @param treeItem treeItem the deleted tree item
+   */
+  private void updateLastSelectedFolderAfterDelete(TreeItem treeItem) {
+    TreeItemFolder tif = (TreeItemFolder) treeItem.getData();
+    if (tif == null || !tif.path.equals(lastSelectedFolderPath)) {
+      return;
+    }
+
+    TreeItem parentItem = treeItem.getParentItem();
+    if (parentItem != null) {
+      TreeItemFolder parentTif = (TreeItemFolder) parentItem.getData();
+      if (parentTif != null) {
+        lastSelectedFolderPath = parentTif.path;
+        return;
+      }
+    }
+    lastSelectedFolderPath = null;
+  }
+
+  /**
+   * Removes the tree item associated with the specified path if it exists.
+   *
+   * @param path path the path of the tree item to remove
+   */
+  private void removeStaleTreeItem(String path) {
+    if (Utils.isEmpty(path)) {
+      return;
+    }
+
+    TreeItem stale = findTreeItem(path);
+    if (stale != null && !stale.isDisposed()) {
+      stale.dispose();
+    }
+  }
+
+  private boolean isAccessibleFolder(String path) {
+    if (Utils.isEmpty(path)) {
+      return false;
+    }
+
+    try {
+      FileObject fileObject = HopVfs.getFileObject(path);
+      return fileObject.exists() && fileObject.isFolder();
+    } catch (Exception e) {
+      return false;
+    }
+  }
+
+  private String getParentFolderPath(String path) {
+    if (Utils.isEmpty(path) || path.equals(rootFolder)) {
+      return null;
+    }
+
+    try {
+      FileObject fileObject = HopVfs.getFileObject(path);
+      if (fileObject.exists()) {
+        FileObject parent = fileObject.getParent();
+        if (parent == null) {
+          return null;
+        }
+        return HopVfs.getFilename(parent);
+      }
+    } catch (Exception ignored) {
+      // Derive parent from the path string when the folder was already 
deleted.
+    }
+
+    String normalized = path.replace('\\', '/');
+    int lastSlash = normalized.lastIndexOf('/');
+    if (lastSlash <= 0) {
+      return rootFolder;
+    }
+    return normalized.substring(0, lastSlash);
+  }
+
+  private void refreshEntireTree() {
     try {
       determineRootFolderName(hopGui);
 
@@ -2879,7 +3075,8 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
         // The TreeMemory will be applied either by restoreTreeState() or 
setExpandedFromMemory()
         if (treeStateBeforeFilter == null) {
           // Only restore from memory if we're not about to restore from saved 
state
-          TreeMemory.setExpandedFromMemory(tree, FILE_EXPLORER_TREE);
+          rootItem.setExpanded(true);
+          restoreTreeItemExpandedFromMemory(rootItem);
         }
       }
 
@@ -3062,7 +3259,6 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
               //
               new TreeItem(childItem, SWT.NONE);
               childItem.setExpanded(false);
-              TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, 
childItem, false);
             }
           }
         }
@@ -3129,6 +3325,35 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
     }
   }
 
+  /**
+   * Restore expand/collapse state for a subtree from {@link TreeMemory}, 
loading lazy folders when
+   * needed.
+   */
+  private void restoreTreeItemExpandedFromMemory(TreeItem item) {
+    if (item == null || item.isDisposed()) {
+      return;
+    }
+
+    TreeItemFolder tif = (TreeItemFolder) item.getData();
+    if (tif != null) {
+      boolean expanded =
+          TreeMemory.getInstance().isExpanded(FILE_EXPLORER_TREE, 
ConstUi.getTreeStrings(item));
+      if (expanded && !tif.loaded && item.getItemCount() == 1) {
+        TreeItem firstChild = item.getItem(0);
+        if (firstChild.getData() == null) {
+          int childDepth = tif.path.equals(rootFolder) ? 0 : tif.depth + 1;
+          refreshFolder(item, tif.path, childDepth);
+          tif.loaded = true;
+        }
+      }
+      item.setExpanded(expanded);
+    }
+
+    for (TreeItem child : item.getItems()) {
+      restoreTreeItemExpandedFromMemory(child);
+    }
+  }
+
   /** Recursively restore the expanded state of a tree item and its children. 
*/
   private void restoreTreeItemState(TreeItem item) {
     if (item == null || item.isDisposed()) {
@@ -3389,6 +3614,47 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
     return null;
   }
 
+  /** Locate a tree item by path, loading and expanding ancestor folders as 
needed. */
+  private TreeItem locateTreeItem(String path) {
+    if (Utils.isEmpty(path)) {
+      return null;
+    }
+    for (TreeItem item : tree.getItems()) {
+      TreeItem found = locateTreeItem(item, path);
+      if (found != null) {
+        return found;
+      }
+    }
+    return null;
+  }
+
+  private TreeItem locateTreeItem(TreeItem item, String path) {
+    TreeItemFolder tif = (TreeItemFolder) item.getData();
+    if (tif != null && tif.path.equals(path)) {
+      return item;
+    }
+
+    if (tif != null && tif.folder && isDescendant(tif.path, path)) {
+      if (!tif.loaded) {
+        BusyIndicator.showWhile(
+            hopGui.getDisplay(),
+            () -> {
+              refreshFolder(item, tif.path, tif.depth + 1);
+              tif.loaded = true;
+            });
+      }
+      item.setExpanded(true);
+      TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, item, true);
+      for (TreeItem child : item.getItems()) {
+        TreeItem found = locateTreeItem(child, path);
+        if (found != null) {
+          return found;
+        }
+      }
+    }
+    return null;
+  }
+
   private TreeItem findTreeItem(TreeItem item, String filename) {
     TreeItemFolder tif = (TreeItemFolder) item.getData();
     if (tif != null && tif.path.equals(filename)) {
@@ -3413,6 +3679,9 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
       if (tif == null) {
         return;
       }
+
+      // update lastSelectedFolderPath
+      recordSelectedFolderPath(tif, selectedItem);
     }
 
     boolean isFolderSelected = tif != null && tif.fileType instanceof 
FolderFileType;
@@ -3435,6 +3704,31 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
     }
   }
 
+  /**
+   * Records the path of the currently selected folder.
+   *
+   * <p>If the selected item is a folder, its path is recorded directly. 
Otherwise, the path of its
+   * parent folder is recorded.
+   *
+   * @param tif the data associated with the selected tree item
+   * @param selectedItem the currently selected tree item
+   */
+  private void recordSelectedFolderPath(TreeItemFolder tif, TreeItem 
selectedItem) {
+    // current selected folder.
+    if (tif.folder) {
+      lastSelectedFolderPath = tif.path;
+      return;
+    }
+
+    TreeItem parent = selectedItem.getParentItem();
+    if (parent != null) {
+      TreeItemFolder parentTif = (TreeItemFolder) parent.getData();
+      if (parentTif != null && parentTif.folder) {
+        lastSelectedFolderPath = parentTif.path;
+      }
+    }
+  }
+
   /**
    * Remove the file type handler from this perspective, from the tab folder. 
This simply tries to
    * remove the item, does not

Reply via email to