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);
       }
     }
   }

Reply via email to