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