This is an automated email from the ASF dual-hosted git repository.
hansva 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 8d27852137 Allow Drag & Drop of files, fixes 2363 (#6685)
8d27852137 is described below
commit 8d278521370ab979a29a85b744571aeb95954c33
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Sat Feb 28 13:09:10 2026 +0100
Allow Drag & Drop of files, fixes 2363 (#6685)
rebase fix
---
.../ui/hopgui/perspective/IFileDropReceiver.java | 33 +++++
.../hop/ui/hopgui/perspective/TabItemReorder.java | 134 ++++++++++++++++++++-
.../perspective/explorer/ExplorerPerspective.java | 105 ++++++++++++++--
3 files changed, 261 insertions(+), 11 deletions(-)
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IFileDropReceiver.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IFileDropReceiver.java
new file mode 100644
index 0000000000..bbe9af23f1
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IFileDropReceiver.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.perspective;
+
+/**
+ * Optional interface for perspectives that can open files when they are
dropped onto the tab folder
+ * (e.g. from the file explorer or from the OS). When the single DropTarget on
the tab folder
+ * receives file data, it delegates to this interface if the perspective
implements it.
+ */
+public interface IFileDropReceiver {
+
+ /**
+ * Open the given file paths as new tabs. Called when the user drops files
onto the canvas.
+ *
+ * @param paths file paths (from FileTransfer, typically from the OS or file
explorer)
+ */
+ void openDroppedFiles(String[] paths);
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabItemReorder.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabItemReorder.java
index 93215200c8..442dfad312 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabItemReorder.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabItemReorder.java
@@ -21,6 +21,7 @@ import java.nio.charset.Charset;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.util.EnvironmentUtils;
+import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.dnd.ByteArrayTransfer;
@@ -31,6 +32,7 @@ import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.DropTargetListener;
+import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.graphics.Font;
@@ -40,11 +42,18 @@ import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Listener;
public class TabItemReorder {
private final IHopPerspective perspective;
private CTabItem dragItem;
+ /**
+ * Tab under the cursor during a tab drag; drop will swap with this tab.
Painted as drop
+ * indicator.
+ */
+ private CTabItem dropTargetTab;
+
public TabItemReorder(IHopPerspective perspective, CTabFolder folder) {
this.perspective = perspective;
@@ -104,33 +113,103 @@ public class TabItemReorder {
}
});
- DropTarget dropTarget = new DropTarget(folder, DND.DROP_MOVE);
- dropTarget.setTransfer(TabTransfer.INSTANCE, TextTransfer.getInstance());
+ DropTarget dropTarget = new DropTarget(folder, DND.DROP_MOVE |
DND.DROP_COPY | DND.DROP_LINK);
+ dropTarget.setTransfer(
+ TabTransfer.INSTANCE, TextTransfer.getInstance(),
FileTransfer.getInstance());
+
+ // Paint a drop indicator (highlight) on the tab we're about to swap with
+ Listener paintListener =
+ event -> {
+ if (dropTargetTab == null || dragItem == null ||
dropTargetTab.isDisposed()) {
+ return;
+ }
+ Rectangle b = dropTargetTab.getBounds();
+ if (b.width <= 0 || b.height <= 0) {
+ return;
+ }
+ GC gc = event.gc;
+ gc.setLineWidth(2);
+
gc.setForeground(folder.getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION));
+ gc.drawRectangle(b.x, b.y, b.width, b.height);
+ };
+ folder.addListener(SWT.Paint, paintListener);
+
dropTarget.addDropListener(
new DropTargetListener() {
+ private boolean isFileDrop;
+
@Override
public void dragEnter(DropTargetEvent event) {
+ isFileDrop = isFileTransferType(event);
+ if (isFileDrop) {
+ event.currentDataType = getFileTransferDataType(event);
+ if (event.detail == DND.DROP_DEFAULT) {
+ event.detail = preferredFileDropOperation(event);
+ }
+ }
handleDragEvent(event);
}
@Override
public void dragLeave(DropTargetEvent event) {
handleDragEvent(event);
+ if (dropTargetTab != null) {
+ dropTargetTab = null;
+ folder.redraw();
+ }
}
@Override
public void dragOperationChanged(DropTargetEvent event) {
+ if (isFileDrop) {
+ if (event.detail == DND.DROP_DEFAULT) {
+ event.detail = preferredFileDropOperation(event);
+ }
+ }
handleDragEvent(event);
}
@Override
public void dragOver(DropTargetEvent event) {
+ if (!isFileDrop) {
+ isFileDrop = isFileTransferType(event);
+ if (isFileDrop) {
+ event.currentDataType = getFileTransferDataType(event);
+ }
+ }
+ if (isFileDrop) {
+ if (event.detail == DND.DROP_DEFAULT) {
+ event.detail = preferredFileDropOperation(event);
+ }
+ }
handleDragEvent(event);
+ // Update drop indicator for tab reorder
+ if (!isFileDrop && dragItem != null && event.detail !=
DND.DROP_NONE) {
+ Point p =
folder.toControl(folder.getDisplay().getCursorLocation());
+ CTabItem over = folder.getItem(p);
+ CTabItem newTarget = (over != null && over != dragItem) ? over :
null;
+ if (newTarget != dropTargetTab) {
+ dropTargetTab = newTarget;
+ folder.redraw();
+ }
+ } else if (dropTargetTab != null) {
+ dropTargetTab = null;
+ folder.redraw();
+ }
}
@Override
public void drop(DropTargetEvent event) {
handleDragEvent(event);
+ if (dropTargetTab != null) {
+ dropTargetTab = null;
+ folder.redraw();
+ }
+ if (event.data instanceof String[] paths
+ && perspective instanceof IFileDropReceiver receiver) {
+ receiver.openDroppedFiles(paths);
+ return;
+ }
if (event.detail == DND.DROP_MOVE) {
moveTabs(folder, event);
}
@@ -141,7 +220,58 @@ public class TabItemReorder {
handleDragEvent(event);
}
+ private boolean isFileTransferType(DropTargetEvent event) {
+ if (event.dataTypes == null) {
+ return false;
+ }
+ FileTransfer ft = FileTransfer.getInstance();
+ for (int i = 0; i < event.dataTypes.length; i++) {
+ if (ft.isSupportedType(event.dataTypes[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private TransferData getFileTransferDataType(DropTargetEvent event) {
+ if (event.dataTypes == null) {
+ return null;
+ }
+ FileTransfer ft = FileTransfer.getInstance();
+ for (int i = 0; i < event.dataTypes.length; i++) {
+ if (ft.isSupportedType(event.dataTypes[i])) {
+ return event.dataTypes[i];
+ }
+ }
+ return null;
+ }
+
+ private int preferredFileDropOperation(DropTargetEvent event) {
+ if ((event.operations & DND.DROP_MOVE) != 0) {
+ return DND.DROP_MOVE;
+ }
+ if ((event.operations & DND.DROP_COPY) != 0) {
+ return DND.DROP_COPY;
+ }
+ if ((event.operations & DND.DROP_LINK) != 0) {
+ return DND.DROP_LINK;
+ }
+ return DND.DROP_NONE;
+ }
+
private void handleDragEvent(DropTargetEvent event) {
+ if (isFileDrop && perspective instanceof IFileDropReceiver) {
+ if (event.dataTypes != null
+ &&
!FileTransfer.getInstance().isSupportedType(event.currentDataType)) {
+ event.currentDataType = getFileTransferDataType(event);
+ }
+ if (event.currentDataType != null
+ &&
FileTransfer.getInstance().isSupportedType(event.currentDataType)) {
+ event.detail = preferredFileDropOperation(event);
+ event.feedback = DND.FEEDBACK_NONE;
+ return;
+ }
+ }
if (!isDropSupported(folder, event)) {
event.detail = DND.DROP_NONE;
} else {
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 535eea6d2b..4c94c99a04 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
@@ -93,6 +93,7 @@ import
org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
import org.apache.hop.ui.hopgui.file.workflow.HopWorkflowFileType;
import org.apache.hop.ui.hopgui.perspective.HopPerspectivePlugin;
+import org.apache.hop.ui.hopgui.perspective.IFileDropReceiver;
import org.apache.hop.ui.hopgui.perspective.IHopPerspective;
import org.apache.hop.ui.hopgui.perspective.TabClosable;
import org.apache.hop.ui.hopgui.perspective.TabCloseHandler;
@@ -124,7 +125,9 @@ import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
@@ -151,7 +154,7 @@ import org.eclipse.swt.widgets.Widget;
name = "i18n::ExplorerPerspective.Name",
description = "i18n::ExplorerPerspective.GuiPlugin.Description")
@SuppressWarnings("java:S1104")
-public class ExplorerPerspective implements IHopPerspective, TabClosable {
+public class ExplorerPerspective implements IHopPerspective, TabClosable,
IFileDropReceiver {
public static final Class<?> PKG = ExplorerPerspective.class; // i18n
@@ -208,6 +211,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
@Getter private Tree tree;
private TreeEditor treeEditor;
private CTabFolder tabFolder;
+ private Composite tabFolderWrapper;
private Control toolBar;
@Getter private GuiMenuWidgets menuWidgets;
private final List<TabItemHandler> items;
@@ -304,7 +308,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
ExplorerPerspectiveConfigSingleton.getConfig().getFileExplorerVisibleByDefault();
fileExplorerPanelVisible = visibleByDefault == null || visibleByDefault;
if (!fileExplorerPanelVisible) {
- sash.setMaximizedControl(tabFolder);
+ sash.setMaximizedControl(tabFolderWrapper);
}
// Refresh the file explorer when a project is activated or updated.
@@ -579,6 +583,8 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
dragSource.setTransfer(fileTransfer);
dragSource.addDragListener(
new DragSourceAdapter() {
+ private Image dragImage;
+
@Override
public void dragStart(DragSourceEvent event) {
ExplorerFile file = getSelectedFile();
@@ -594,6 +600,33 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
// Used by dragOver
dragFile = file.getFilename();
+
+ // Set an explicit drag image to avoid macOS NPE in
TreeDragSourceEffect when the
+ // native side requests the default tree drag image
(dragImageFromListener.handle null).
+ if (EnvironmentUtils.getInstance().isWeb()) {
+ event.image = GuiResource.getInstance().getImageHop();
+ } else {
+ TreeItem[] selection = tree.getSelection();
+ if (selection != null && selection.length > 0) {
+ Rectangle bounds = selection[0].getBounds();
+ int w = Math.max(1, bounds.width);
+ int h = Math.max(1, bounds.height);
+ try {
+ dragImage = new Image(hopGui.getDisplay(), w, h);
+ GC gc = new GC(tree);
+ try {
+ gc.copyArea(dragImage, bounds.x, bounds.y);
+ } finally {
+ gc.dispose();
+ }
+ event.image = dragImage;
+ } catch (Exception e) {
+ hopGui
+ .getLog()
+ .logDebug(getClass().getSimpleName(), "Could not create
drag image", e);
+ }
+ }
+ }
}
@Override
@@ -606,6 +639,10 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
@Override
public void dragFinished(DragSourceEvent event) {
+ if (dragImage != null) {
+ dragImage.dispose();
+ dragImage = null;
+ }
dragFile = null;
}
});
@@ -766,7 +803,6 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
}
/**
- * This is called when a user expands a folder. We only need to lazily load
the contents of the
* folder if it's not loaded already. To keep track of this we have a flag
called "loaded" in the
* item data.
*/
@@ -1106,7 +1142,13 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
}
protected void createTabFolder(Composite parent) {
- tabFolder = new CTabFolder(parent, SWT.MULTI | SWT.BORDER);
+ tabFolderWrapper = new Composite(parent, SWT.NONE);
+ tabFolderWrapper.setLayout(new FormLayout());
+ tabFolderWrapper.setLayoutData(new FormDataBuilder().fullSize().result());
+ PropsUi.setLook(tabFolderWrapper);
+
+ tabFolder = new CTabFolder(tabFolderWrapper, SWT.MULTI | SWT.BORDER);
+ tabFolder.setLayoutData(new FormDataBuilder().fullSize().result());
tabFolder.addListener(
SWT.Selection,
e -> {
@@ -1138,7 +1180,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
SWT.Selection,
e -> {
if (sash.getMaximizedControl() == null) {
- sash.setMaximizedControl(tabFolder);
+ sash.setMaximizedControl(tabFolderWrapper);
item.setImage(GuiResource.getInstance().getImageMinimizePanel());
} else {
sash.setMaximizedControl(null);
@@ -1150,11 +1192,56 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
new TabCloseHandler(this);
- // Support reorder tab item
+ // Support reorder tab item (also handles file drops when perspective
implements
+ // IFileDropReceiver)
//
new TabItemReorder(this, tabFolder);
}
+ @Override
+ public void openDroppedFiles(String[] paths) {
+ if (paths == null || paths.length == 0) {
+ return;
+ }
+ List<String> errors = new ArrayList<>();
+ IHopFileTypeHandler lastOpened = null;
+ for (String path : paths) {
+ try {
+ FileObject fileObject = HopVfs.getFileObject(path);
+ if (fileObject.isFolder()) {
+ continue;
+ }
+ String filename = HopVfs.getFilename(fileObject);
+ IHopFileType fileType = getFileType(filename);
+ if (fileType instanceof FolderFileType || !fileType.supportsOpening())
{
+ continue;
+ }
+ IHopFileTypeHandler handler = fileType.openFile(hopGui, filename,
hopGui.getVariables());
+ if (handler != null && !(handler instanceof EmptyHopFileTypeHandler)) {
+ lastOpened = handler;
+ handler.updateGui();
+ }
+ } catch (Exception e) {
+ errors.add(path + ": " + e.getMessage());
+ hopGui.getLog().logError("Error opening dropped file '" + path + "'",
e);
+ }
+ }
+ if (!errors.isEmpty()) {
+ String message = String.join("\n", errors);
+ MessageBox messageBox = new MessageBox(hopGui.getShell(),
SWT.ICON_WARNING | SWT.OK);
+ messageBox.setText(BaseMessages.getString(PKG,
"ExplorerPerspective.Error.OpenFile.Header"));
+ messageBox.setMessage(
+ BaseMessages.getString(PKG,
"ExplorerPerspective.Error.OpenFile.Message")
+ + Const.CR
+ + Const.CR
+ + message);
+ messageBox.open();
+ }
+ if (lastOpened != null) {
+ setActiveFileTypeHandler(lastOpened);
+ }
+ }
+
protected TabItemHandler findTabItemHandler(String filename) {
if (filename != null) {
for (TabItemHandler item : items) {
@@ -2571,8 +2658,8 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
sash.setMaximizedControl(null);
sash.setWeights(20, 80);
} else {
- // Hide the file explorer panel - maximize the tab folder
- sash.setMaximizedControl(tabFolder);
+ // Hide the file explorer panel - maximize the tab folder area
+ sash.setMaximizedControl(tabFolderWrapper);
}
}
@@ -2642,7 +2729,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable {
sash.setMaximizedControl(null);
sash.setWeights(20, 80);
} else {
- sash.setMaximizedControl(tabFolder);
+ sash.setMaximizedControl(tabFolderWrapper);
}
}
}