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 66d37b5718 Add DnD effects to hop web, fixes #8499 (#8512)
66d37b5718 is described below
commit 66d37b571893382851772512af003621471bf727
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Mon Sep 21 18:54:26 2026 +0200
Add DnD effects to hop web, fixes #8499 (#8512)
---
.../main/java/org/apache/hop/ui/hopgui/HopWeb.java | 1 +
.../org/apache/hop/ui/hopgui/HopWebEntryPoint.java | 2 +
.../org/apache/hop/ui/hopgui/dark-mode.css | 8 +
.../apache/hop/ui/hopgui/dnd-cursor-passthrough.js | 51 +++++
.../org/apache/hop/ui/hopgui/light-mode.css | 8 +
.../hop/ui/hopgui/perspective/IHopPerspective.java | 25 +++
.../ui/hopgui/perspective/ShellTabDropFrame.java | 99 +++++++++
.../hop/ui/hopgui/perspective/TabDropFrame.java | 52 +++++
.../hop/ui/hopgui/perspective/TabItemReorder.java | 238 +++++++++++++--------
.../hop/ui/hopgui/perspective/WebTabDropFrame.java | 69 ++++++
.../perspective/explorer/ExplorerPerspective.java | 22 +-
11 files changed, 483 insertions(+), 92 deletions(-)
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
index df5c5ab51f..e3cd2da161 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
@@ -171,6 +171,7 @@ public class HopWeb implements ApplicationConfiguration {
"org/apache/hop/ui/hopgui/context-dialog-svg.js",
"org/apache/hop/ui/hopgui/log-console.js",
"org/apache/hop/ui/hopgui/gc-pending-images.js",
+ "org/apache/hop/ui/hopgui/dnd-cursor-passthrough.js",
"org/apache/hop/ui/hopgui/monaco-editor.js",
"org/apache/hop/ui/hopgui/mac-command-keys.js")
.forEach(
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
index 09c143c21b..bbac59e6ae 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
@@ -200,6 +200,8 @@ public class HopWebEntryPoint extends AbstractEntryPoint {
jsLoader.require(resourceManager.getLocation("js/log-console.js"));
// RAP's GC leaves image onload handlers alive after dispose; see the
script.
jsLoader.require(resourceManager.getLocation("js/gc-pending-images.js"));
+ // RAP's drag cursor icon would otherwise catch the pointer on a fast
move; see the script.
+
jsLoader.require(resourceManager.getLocation("js/dnd-cursor-passthrough.js"));
jsLoader.require(resourceManager.getLocation("js/monaco-editor.js"));
// Map Mac Command key to Ctrl so RAP ACTIVE_KEYS (CTRL+S etc.) match when
user presses Cmd+S
String macKeysLocation =
resourceManager.getLocation("js/mac-command-keys.js");
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/dark-mode.css
b/rap/src/main/resources/org/apache/hop/ui/hopgui/dark-mode.css
index f4226a5b0e..f5f2fbbcaf 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/dark-mode.css
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/dark-mode.css
@@ -2522,6 +2522,14 @@ FileUpload-FocusIndicator {
opacity: 1;
}
+/* Frame marking where a dragged tab would land (TabItemReorder /
WebTabDropFrame) */
+
+Composite.hopDropFrame {
+ background-color: transparent;
+ border: 3px solid #3584e4;
+ padding: 0;
+}
+
/* Sidebar button styling */
Composite.sidebarButton {
diff --git
a/rap/src/main/resources/org/apache/hop/ui/hopgui/dnd-cursor-passthrough.js
b/rap/src/main/resources/org/apache/hop/ui/hopgui/dnd-cursor-passthrough.js
new file mode 100644
index 0000000000..d493e82130
--- /dev/null
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/dnd-cursor-passthrough.js
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+//# sourceURL=dnd-cursor-passthrough.js
+
+/**
+ * Keeps RAP's drag-and-drop cursor icon from catching the pointer it follows.
+ *
+ * During a drag the RAP client draws the operation icon (move / copy /
no-drop) as a small
+ * absolutely positioned widget 5 px right of and 15 px below the pointer, on
top of everything
+ * else. Its element is an ordinary div, so a quick downward move lands the
pointer inside it:
+ * the drop target is then resolved from the icon, which has none, and the
client tells the
+ * server the drag left the folder or canvas it was over. Tab drags towards
the bottom of a
+ * folder lost their drop that way (RAP 4.7.0 / 4.8.0, unchanged upstream).
+ *
+ * The icon is feedback, not a target: let the pointer pass through it.
+ */
+(function () {
+ "use strict";
+
+ if (typeof rwt === "undefined" || !rwt.event ||
!rwt.event.DragAndDropHandler) {
+ return;
+ }
+ var handler = rwt.event.DragAndDropHandler.getInstance();
+ var renderCursor = handler._renderCursor;
+ if (typeof renderCursor !== "function" || renderCursor.hopPassesPointer) {
+ return;
+ }
+ handler._renderCursor = function () {
+ renderCursor.apply(this, arguments);
+ var cursor = this.__cursor;
+ var element = cursor && cursor.getElement ? cursor.getElement() : null;
+ if (element && element.style.pointerEvents !== "none") {
+ element.style.pointerEvents = "none";
+ }
+ };
+ handler._renderCursor.hopPassesPointer = true;
+})();
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/light-mode.css
b/rap/src/main/resources/org/apache/hop/ui/hopgui/light-mode.css
index 110c270ff7..dc1a3d9e86 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/light-mode.css
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/light-mode.css
@@ -2528,6 +2528,14 @@ FileUpload-FocusIndicator {
opacity: 1;
}
+/* Frame marking where a dragged tab would land (TabItemReorder /
WebTabDropFrame) */
+
+Composite.hopDropFrame {
+ background-color: transparent;
+ border: 3px solid #3584e4;
+ padding: 0;
+}
+
/* Sidebar button styling */
Composite.sidebarButton {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IHopPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IHopPerspective.java
index 3d5c84836b..27c2716b5f 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IHopPerspective.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IHopPerspective.java
@@ -25,6 +25,7 @@ import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.file.empty.EmptyHopFileTypeHandler;
import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
@@ -148,6 +149,30 @@ public interface IHopPerspective extends
IActionContextHandlersProvider {
// Do nothing by default
}
+ /**
+ * Remember the tab currently being dragged, or {@code null} when a drag
ends. Every folder of a
+ * perspective has its own drop target, but only the folder the drag started
on knows the tab; the
+ * others read it here so a drop from one pane into another can complete.
This is what makes
+ * cross-pane tab drops work in Hop Web, where the transfer types are not on
the drop event by the
+ * time the drop is accepted. Held on the (per-session) perspective, so two
sessions never see
+ * each other's drag.
+ *
+ * @param tabItem the tab being dragged, or null
+ */
+ default void setDraggedTabItem(CTabItem tabItem) {
+ // Do nothing by default
+ }
+
+ /**
+ * The tab currently being dragged in this perspective, or null when none is.
+ *
+ * @return the dragged tab, or null
+ * @see #setDraggedTabItem(CTabItem)
+ */
+ default CTabItem getDraggedTabItem() {
+ return null;
+ }
+
/** Drop into the folder itself (no split). */
int DROP_ZONE_CENTER = 0;
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/ShellTabDropFrame.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/ShellTabDropFrame.java
new file mode 100644
index 0000000000..9c96ac873e
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/ShellTabDropFrame.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.Region;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * The desktop drop frame: a floating {@code SWT.NO_TRIM | SWT.ON_TOP} shell
shaped into a hollow
+ * rectangle with a {@link Region}. The cut-out centre is not part of the
window, so the drag cursor
+ * passes through it to the folder underneath (no enter/leave oscillation),
and only the thin border
+ * is painted (no compositing flicker).
+ */
+final class ShellTabDropFrame implements TabDropFrame {
+
+ private Shell shell;
+
+ /** The frame-shaped region applied to {@link #shell}; disposed with it. */
+ private Region region;
+
+ @Override
+ public void show(CTabFolder folder, Rectangle bounds) {
+ if (folder.isDisposed() || bounds.width <= 0 || bounds.height <= 0) {
+ hide();
+ return;
+ }
+ Point topLeft = folder.toDisplay(bounds.x, bounds.y);
+ Rectangle onDisplay = new Rectangle(topLeft.x, topLeft.y, bounds.width,
bounds.height);
+ try {
+ if (shell == null || shell.isDisposed()) {
+ shell = new Shell(folder.getShell(), SWT.NO_TRIM | SWT.ON_TOP);
+
shell.setBackground(folder.getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION));
+ shell.addDisposeListener(e -> disposeRegion());
+ }
+ shell.setBounds(onDisplay);
+ applyFrameRegion(onDisplay.width, onDisplay.height);
+ if (!shell.getVisible()) {
+ shell.setVisible(true);
+ }
+ } catch (Exception e) {
+ hide();
+ }
+ }
+
+ @Override
+ public void hide() {
+ if (shell != null && !shell.isDisposed() && shell.getVisible()) {
+ shell.setVisible(false);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (shell != null && !shell.isDisposed()) {
+ shell.dispose();
+ }
+ shell = null;
+ disposeRegion();
+ }
+
+ /** Shape the shell as a hollow rectangle frame of the given size. */
+ private void applyFrameRegion(int width, int height) {
+ int border = Math.max(3, Math.min(8, Math.min(width, height) / 12));
+ Region frame = new Region(shell.getDisplay());
+ frame.add(0, 0, width, height);
+ if (width > 2 * border && height > 2 * border) {
+ frame.subtract(border, border, width - 2 * border, height - 2 * border);
+ }
+ shell.setRegion(frame);
+ disposeRegion();
+ region = frame;
+ }
+
+ private void disposeRegion() {
+ if (region != null && !region.isDisposed()) {
+ region.dispose();
+ }
+ region = null;
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabDropFrame.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabDropFrame.java
new file mode 100644
index 0000000000..5283c75151
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabDropFrame.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+import org.apache.hop.ui.util.EnvironmentUtils;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The frame {@link TabItemReorder} draws while a tab is dragged over a
folder: around the tab the
+ * drop would swap with, or around the half of the folder an edge drop would
split off. The desktop
+ * draws it as a floating shell, Hop Web as a child of the folder; see {@link
#create()}.
+ */
+interface TabDropFrame {
+
+ /**
+ * Show the frame around a rectangle of the folder, moving it when it is
already up.
+ *
+ * @param folder the folder the drag is over
+ * @param bounds the rectangle to frame, in folder coordinates
+ */
+ void show(CTabFolder folder, Rectangle bounds);
+
+ /** Take the frame down; a no-op when it is not up. */
+ void hide();
+
+ /** Release whatever the frame holds. */
+ void dispose();
+
+ /** The frame for the platform Hop runs on. */
+ static TabDropFrame create() {
+ if (EnvironmentUtils.getInstance().isWeb()) {
+ return new WebTabDropFrame();
+ }
+ return new ShellTabDropFrame();
+ }
+}
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 5ccb057069..fcb26b3b30 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
@@ -41,12 +41,27 @@ import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
-import org.eclipse.swt.graphics.Region;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Listener;
-import org.eclipse.swt.widgets.Shell;
+/**
+ * Drag-and-drop reordering and splitting of editor tabs, on the desktop and
in Hop Web alike.
+ *
+ * <ul>
+ * <li>Drop a tab on another tab of the same folder to reorder them.
+ * <li>Drop a tab in the outer {@link #EDGE_FRACTION} band of a folder to
split it off into a new
+ * pane on that side (right / bottom / left / top).
+ * <li>Drop a tab into another open pane to move it there.
+ * </ul>
+ *
+ * <p>All of it works in Hop Web too, with three RAP-specific adjustments: the
drop frame is a child
+ * composite of the folder rather than a floating shell ({@link
TabDropFrame}); the tab being
+ * dragged is settled from the MouseDown that RAP delivers just after the
DragStart ({@link
+ * #settleDragItem}); and the tab is shared across the panes of a perspective
({@link
+ * #activeDragItem()}), because a pane that did not start the drag has no
{@code dragItem} of its
+ * own and RAP has dropped the transfer types from the event by the time the
drop is accepted.
+ */
public class TabItemReorder {
/** Fraction of the folder width/height near an edge that triggers a
split-on-drop. */
@@ -63,6 +78,14 @@ public class TabItemReorder {
*/
private CTabItem mouseDownItem;
+ /**
+ * True from {@code dragStart} to {@code dragFinished} of a drag that
started on this folder. Hop
+ * Web needs it: RAP hands the folder its {@code DragStart} before the
{@code MouseDown} of the
+ * same request, so {@link #dragItem} can only be settled from {@link
#mouseDownItem} once the
+ * drag is under way, and only by the folder the drag started on (see {@link
#settleDragItem}).
+ */
+ private boolean dragging;
+
/**
* Tab under the cursor during a tab drag; drop will swap with this tab.
Painted as drop
* indicator.
@@ -80,31 +103,39 @@ public class TabItemReorder {
private int lastDragOverZone = IHopPerspective.DROP_ZONE_CENTER;
/**
- * Overlay marking where a split-on-drop would land; a hollow frame (see
{@link #overlayRegion})
- * so the cursor passes through its centre to the folder underneath instead
of stealing the drag.
+ * The frame marking where the drop would land: around the tab to swap with
(Hop Web only, the
+ * desktop paints that one on the folder) or around the half of the folder
an edge drop would
+ * split off.
*/
- private Shell zoneOverlay;
-
- /** The frame-shaped region applied to {@link #zoneOverlay}; disposed with
it. */
- private Region overlayRegion;
+ private final TabDropFrame dropFrame = TabDropFrame.create();
- /** Zone/folder the overlay currently reflects, so we only touch the Shell
when they change. */
+ /** What the frame currently shows, so it is only touched when that changes.
*/
private int shownZone = IHopPerspective.DROP_ZONE_CENTER;
+ private CTabItem shownTab;
private CTabFolder shownFolder;
public TabItemReorder(IHopPerspective perspective, CTabFolder folder) {
this.perspective = perspective;
+ folder.addListener(SWT.Dispose, e -> dropFrame.dispose());
// Remember which tab the pointer went down on: dragStart can't reliably
re-derive it from the
- // cursor location on the first macOS drag of a session.
+ // cursor location on the first macOS drag of a session, and in Hop Web it
runs before this
+ // listener. Forget it again on release so that a later drag never picks
up a stale tab.
folder.addListener(
SWT.MouseDown,
e -> {
if (e.button == 1) {
mouseDownItem = folder.getItem(new Point(e.x, e.y));
+ // In Hop Web, DragStart of the same request may have already run
and left the drag
+ // waiting for this tab (it runs before this listener). Settle it
now, in the same
+ // request as DragStart, so even a fast drop - one that reaches
the target pane before
+ // any drag-over settles the tab there - already has it. Otherwise
the merge only works
+ // when the pointer lingers long enough for a source drag-over to
fire first.
+ settleDragItem(folder);
}
});
+ folder.addListener(SWT.MouseUp, e -> mouseDownItem = null);
final DragSource source = new DragSource(folder, DND.DROP_MOVE);
source.setTransfer(TabTransfer.INSTANCE);
@@ -115,13 +146,22 @@ public class TabItemReorder {
@Override
public void dragStart(DragSourceEvent event) {
dragItem = itemBeingDragged(folder);
+ perspective.setDraggedTabItem(dragItem);
if (dragItem == null) {
+ if (EnvironmentUtils.getInstance().isWeb()) {
+ // RAP delivers this before the MouseDown of the same request,
and by the time the
+ // drag threshold is passed the pointer has often left the tab
strip. Keep the drag
+ // alive: settleDragItem picks the tab up from that MouseDown
on the next event.
+ dragging = true;
+ return;
+ }
// Couldn't identify the tab (e.g. drag not started from a tab):
cancel cleanly rather
// than begin a data-less drag that would silently do nothing on
drop.
event.doit = false;
return;
}
+ dragging = true;
Rectangle columnBounds = dragItem.getBounds();
if (dragImage != null) {
dragImage.dispose();
@@ -149,12 +189,16 @@ public class TabItemReorder {
@Override
public void dragSetData(DragSourceEvent event) {
+ settleDragItem(folder);
event.data = dragItem;
}
@Override
public void dragFinished(DragSourceEvent event) {
dragItem = null;
+ mouseDownItem = null;
+ dragging = false;
+ perspective.setDraggedTabItem(null);
if (EnvironmentUtils.getInstance().isWeb()) {
return;
}
@@ -172,7 +216,8 @@ public class TabItemReorder {
FileTransfer.getInstance(),
MetadataTransfer.INSTANCE);
- // Paint a drop indicator (highlight) on the tab we're about to swap with
+ // Paint a drop indicator (highlight) on the tab we're about to swap with.
RAP delivers no paint
+ // events for a folder; Hop Web shows the drop frame around that tab
instead (updateDropFrame).
Listener paintListener =
event -> {
if (dropTargetTab == null || dragItem == null ||
dropTargetTab.isDisposed()) {
@@ -196,6 +241,7 @@ public class TabItemReorder {
@Override
public void dragEnter(DropTargetEvent event) {
+ settleDragItem(folder);
lastDragOverZone = IHopPerspective.DROP_ZONE_CENTER;
isFileDrop = isFileTransferType(event);
isMetadataDrop = isMetadataTransferType(event);
@@ -229,6 +275,7 @@ public class TabItemReorder {
@Override
public void dragOver(DropTargetEvent event) {
+ settleDragItem(folder);
if (!isFileDrop && !isMetadataDrop) {
isFileDrop = isFileTransferType(event);
isMetadataDrop = isMetadataTransferType(event);
@@ -248,7 +295,9 @@ public class TabItemReorder {
handleDragEvent(event);
// Update drop indicator (tab reorder) and split zone (edge drop)
feedback.
boolean tabDrag =
- !isFileDrop && !isMetadataDrop && (dragItem != null ||
hasActiveTabTransfer(event));
+ !isFileDrop
+ && !isMetadataDrop
+ && (activeDragItem() != null ||
hasActiveTabTransfer(event));
if (tabDrag && event.detail != DND.DROP_NONE) {
Point p = eventPoint(folder, event);
CTabItem over = folder.getItem(p);
@@ -272,7 +321,7 @@ public class TabItemReorder {
// Remember the zone while we're genuinely over the folder;
drop() falls back to this
// if its own event coordinates come through degenerate.
lastDragOverZone = newZone;
- updateZoneOverlay(folder);
+ updateDropFrame(folder);
} else {
clearDropFeedback(folder);
}
@@ -280,6 +329,7 @@ public class TabItemReorder {
@Override
public void drop(DropTargetEvent event) {
+ settleDragItem(folder);
handleDragEvent(event);
// Resolve the drop point from the event's own display
coordinates, which — unlike
// Display.getCursorLocation() — are the actual drop location and
are reliable even on
@@ -302,7 +352,7 @@ public class TabItemReorder {
receiver.openDroppedFiles(paths);
return;
}
- boolean tabDrag = dragItem != null || hasActiveTabTransfer(event);
+ boolean tabDrag = activeDragItem() != null ||
hasActiveTabTransfer(event);
if (LogChannel.UI.isDebug() && tabDrag) {
LogChannel.UI.logDebug(
"Tab drop: detail="
@@ -407,7 +457,13 @@ public class TabItemReorder {
}
private boolean isDropSupported(CTabFolder folder, DropTargetEvent
event) {
- if (dragItem != null && !dragItem.isDisposed()) {
+ CTabItem item = activeDragItem();
+ if (item != null) {
+ // A tab dragged in from another pane can always land here (join
or split), and its
+ // drop point maps unreliably across panes in Hop Web, so accept
without resolving it.
+ if (item.getParent() != folder) {
+ return true;
+ }
// Use the event's own coordinates rather than
Display.getCursorLocation(): the latter
// can read stale on the first macOS drag, wrongly collapsing an
edge drop to CENTER
// and forcing event.detail to DROP_NONE (the "first drop does
nothing" bug).
@@ -415,11 +471,9 @@ public class TabItemReorder {
if (folder.getItem(point) != null) {
return true;
}
- // Allow an edge drop to split. A same-folder split must leave a
tab behind (>1 tab);
- // a cross-folder drop can always land, so only require an edge
zone there.
- boolean sameFolder = dragItem.getParent() == folder;
+ // A same-folder split must leave a tab behind (>1 tab).
boolean edge = computeDropZone(folder, point) !=
IHopPerspective.DROP_ZONE_CENTER;
- return edge && (!sameFolder || folder.getItemCount() > 1);
+ return edge && folder.getItemCount() > 1;
}
return hasActiveTabTransfer(event);
}
@@ -474,9 +528,44 @@ public class TabItemReorder {
&& mouseDownItem.getParent() == folder) {
return mouseDownItem;
}
+ if (EnvironmentUtils.getInstance().isWeb()) {
+ // The pointer is wherever the drag threshold was passed, which need not
be the pressed tab
+ // (or any tab): the MouseDown that follows is the only reliable source,
see settleDragItem.
+ return null;
+ }
return
folder.getItem(folder.toControl(folder.getDisplay().getCursorLocation()));
}
+ /**
+ * Settle the tab being dragged once the drag is under way: in Hop Web
{@code dragStart} may have
+ * run before the MouseDown that names the tab (see {@link #dragging}). Only
the folder the drag
+ * started on does this; another folder's last pressed tab has nothing to do
with the drag.
+ */
+ private void settleDragItem(CTabFolder folder) {
+ if (dragging
+ && dragItem == null
+ && mouseDownItem != null
+ && !mouseDownItem.isDisposed()
+ && mouseDownItem.getParent() == folder) {
+ dragItem = mouseDownItem;
+ perspective.setDraggedTabItem(dragItem);
+ }
+ }
+
+ /**
+ * The tab this drag is carrying: {@link #dragItem} on the folder the drag
started on, or the tab
+ * the perspective is holding on any other folder of the same perspective.
This is what lets a
+ * drop into a <em>different</em> pane complete: that pane's own {@code
dragItem} is null, and in
+ * Hop Web the transfer types are no longer on the drop event by the time
the drop is accepted.
+ */
+ private CTabItem activeDragItem() {
+ if (dragItem != null && !dragItem.isDisposed()) {
+ return dragItem;
+ }
+ CTabItem shared = perspective.getDraggedTabItem();
+ return (shared != null && !shared.isDisposed()) ? shared : null;
+ }
+
private void moveTabs(CTabFolder folder, DropTargetEvent event, int zone,
Point dropPoint) {
CTabItem sourceItem = this.dragItem;
@@ -490,6 +579,16 @@ public class TabItemReorder {
}
}
+ // A drop into another pane: this folder was not the drag source, so its
own dragItem is null
+ // and (in Hop Web) the transfer may not have delivered event.data. Fall
back to the tab the
+ // perspective is holding for the drag.
+ if (sourceItem == null) {
+ CTabItem shared = perspective.getDraggedTabItem();
+ if (shared != null && !shared.isDisposed()) {
+ sourceItem = shared;
+ }
+ }
+
if (sourceItem == null) {
return;
}
@@ -619,12 +718,6 @@ public class TabItemReorder {
* side maps to that edge (split), the middle maps to {@code CENTER} (drop
into the folder as-is).
*/
private int computeDropZone(CTabFolder folder, Point p) {
- // Drag-to-split relies on native DnD + floating overlays, which don't
behave under RAP, so on
- // the web every drop is a plain centre drop (no edge splits). This is the
single choke point
- // for edge zones (drag feedback, drop routing and isDropSupported all go
through here).
- if (EnvironmentUtils.getInstance().isWeb()) {
- return IHopPerspective.DROP_ZONE_CENTER;
- }
Point size = folder.getSize();
if (size.x <= 0 || size.y <= 0) {
return IHopPerspective.DROP_ZONE_CENTER;
@@ -663,86 +756,50 @@ public class TabItemReorder {
}
}
dropZone = IHopPerspective.DROP_ZONE_CENTER;
- hideZoneOverlay();
+ hideDropFrame();
}
- /** Show (or move) the translucent overlay marking where an edge-drop split
would land. */
- private void updateZoneOverlay(CTabFolder folder) {
- if (EnvironmentUtils.getInstance().isWeb()) {
- return; // Floating overlays have no faithful equivalent under RAP.
+ /**
+ * Show (or move) the frame marking where the drop would land, or take it
down when the drop is a
+ * plain centre drop with nothing to mark.
+ */
+ private void updateDropFrame(CTabFolder folder) {
+ if (folder.isDisposed()) {
+ hideDropFrame();
+ return;
}
- if (dropZone == IHopPerspective.DROP_ZONE_CENTER || folder.isDisposed()) {
- hideZoneOverlay();
+ // The desktop paints the tab highlight itself; Hop Web gets no paint
events and frames the tab.
+ CTabItem tab = EnvironmentUtils.getInstance().isWeb() ? dropTargetTab :
null;
+ int zone = tab != null ? IHopPerspective.DROP_ZONE_CENTER : dropZone;
+ if (tab == null && zone == IHopPerspective.DROP_ZONE_CENTER) {
+ hideDropFrame();
return;
}
- // Nothing changed since the overlay was last shown: leave the Shell
untouched. Repositioning it
- // on every drag-over event (they fire continuously) is what makes it
flicker.
- if (dropZone == shownZone
- && folder == shownFolder
- && zoneOverlay != null
- && !zoneOverlay.isDisposed()
- && zoneOverlay.getVisible()) {
+ // Nothing changed since the frame was last shown: leave it alone. Moving
it on every drag-over
+ // event (they fire continuously) is what makes it flicker.
+ if (tab == shownTab && zone == shownZone && folder == shownFolder) {
return;
}
- Rectangle r = zoneRectangleDisplay(folder, dropZone);
+ Rectangle r = tab != null ? tab.getBounds() : zoneRectangle(folder, zone);
if (r == null || r.width <= 0 || r.height <= 0) {
- hideZoneOverlay();
+ hideDropFrame();
return;
}
- try {
- if (zoneOverlay == null || zoneOverlay.isDisposed()) {
- zoneOverlay = new Shell(folder.getShell(), SWT.NO_TRIM | SWT.ON_TOP);
-
zoneOverlay.setBackground(folder.getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION));
- zoneOverlay.addDisposeListener(e -> disposeOverlayRegion());
- }
- zoneOverlay.setBounds(r);
- applyFrameRegion(r.width, r.height);
- if (!zoneOverlay.getVisible()) {
- zoneOverlay.setVisible(true);
- }
- shownZone = dropZone;
- shownFolder = folder;
- } catch (Exception e) {
- hideZoneOverlay();
- }
- }
-
- /**
- * Shape {@link #zoneOverlay} as a hollow rectangle frame of the given size.
The cut-out centre is
- * not part of the window, so the drag cursor passes through it to the
folder underneath (no
- * enter/leave oscillation), and only the thin border is painted (no
compositing flicker).
- */
- private void applyFrameRegion(int width, int height) {
- int border = Math.max(3, Math.min(8, Math.min(width, height) / 12));
- Region region = new Region(zoneOverlay.getDisplay());
- region.add(0, 0, width, height);
- if (width > 2 * border && height > 2 * border) {
- region.subtract(border, border, width - 2 * border, height - 2 * border);
- }
- zoneOverlay.setRegion(region);
- disposeOverlayRegion();
- overlayRegion = region;
+ dropFrame.show(folder, r);
+ shownTab = tab;
+ shownZone = zone;
+ shownFolder = folder;
}
- private void disposeOverlayRegion() {
- if (overlayRegion != null && !overlayRegion.isDisposed()) {
- overlayRegion.dispose();
- }
- overlayRegion = null;
- }
-
- private void hideZoneOverlay() {
- if (zoneOverlay != null && !zoneOverlay.isDisposed() &&
zoneOverlay.getVisible()) {
- zoneOverlay.setVisible(false);
- }
+ private void hideDropFrame() {
+ dropFrame.hide();
+ shownTab = null;
shownZone = IHopPerspective.DROP_ZONE_CENTER;
shownFolder = null;
}
- /**
- * The half of the folder (in display coordinates) that a split-drop in
{@code zone} would use.
- */
- private Rectangle zoneRectangleDisplay(CTabFolder folder, int zone) {
+ /** The half of the folder (in folder coordinates) that a split-drop in
{@code zone} would use. */
+ private Rectangle zoneRectangle(CTabFolder folder, int zone) {
Point size = folder.getSize();
if (size.x <= 0 || size.y <= 0) {
return null;
@@ -766,8 +823,7 @@ public class TabItemReorder {
return null;
}
}
- Point topLeft = folder.toDisplay(x, y);
- return new Rectangle(topLeft.x, topLeft.y, w, h);
+ return new Rectangle(x, y, w, h);
}
private void updateTabItemHandler(IHopFileTypeHandler fileTypeHandler,
CTabItem tabItem) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/WebTabDropFrame.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/WebTabDropFrame.java
new file mode 100644
index 0000000000..b1acad7de0
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/WebTabDropFrame.java
@@ -0,0 +1,69 @@
+/*
+ * 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;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * The Hop Web drop frame: a child composite of the folder, drawn by the {@code
+ * Composite.hopDropFrame} rule of the theme CSS as a border with a
transparent centre. RAP has no
+ * shell regions, but it does not need one here: its client resolves the drop
target by walking up
+ * from the element under the pointer, so a child of the folder hands the drag
to the folder instead
+ * of stealing it. The composite only exists while a drag is over the folder.
+ */
+final class WebTabDropFrame implements TabDropFrame {
+
+ /** Custom variant of the frame composite, styled in the Hop Web theme CSS.
*/
+ static final String CUSTOM_VARIANT = "hopDropFrame";
+
+ private static final String RWT_CUSTOM_VARIANT =
"org.eclipse.rap.rwt.customVariant";
+
+ private Composite frame;
+
+ @Override
+ public void show(CTabFolder folder, Rectangle bounds) {
+ if (folder.isDisposed() || bounds.width <= 0 || bounds.height <= 0) {
+ hide();
+ return;
+ }
+ if (frame == null || frame.isDisposed() || frame.getParent() != folder) {
+ dispose();
+ frame = new Composite(folder, SWT.NONE);
+ frame.setData(RWT_CUSTOM_VARIANT, CUSTOM_VARIANT);
+ }
+ frame.setBounds(bounds);
+ frame.moveAbove(null);
+ frame.setVisible(true);
+ }
+
+ @Override
+ public void hide() {
+ dispose();
+ }
+
+ @Override
+ public void dispose() {
+ if (frame != null && !frame.isDisposed()) {
+ frame.dispose();
+ }
+ frame = null;
+ }
+}
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 6e1e9e3d01..9091d11fc3 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
@@ -5413,10 +5413,18 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Override
public void onTabMovedBetweenFolders(CTabFolder sourceFolder, CTabFolder
targetFolder) {
activeTabFolder = targetFolder;
- reclaimFolder(sourceFolder);
if (EnvironmentUtils.getInstance().isWeb()) {
+ // A drop that empties the source pane wants to collapse it, but we are
still inside the drop
+ // handler of the target pane's drag-and-drop request. Disposing the
source pane (and with it
+ // its drag source) now makes RAP render a disposed widget at the end of
that same request -
+ // "Widget is disposed". Collapse it on the next tick, once the drop
request is done. The
+ // desktop disposes in place: native DnD does not revisit the source
pane after the drop.
+ CTabFolder folderToReclaim = sourceFolder;
+ hopGui.getDisplay().asyncExec(() -> reclaimFolder(folderToReclaim));
notifyZoomHandlerForActiveTab();
updateWebUrlForActiveTab();
+ } else {
+ reclaimFolder(sourceFolder);
}
}
@@ -5427,6 +5435,18 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
}
+ private CTabItem draggedTabItem;
+
+ @Override
+ public void setDraggedTabItem(CTabItem tabItem) {
+ this.draggedTabItem = tabItem;
+ }
+
+ @Override
+ public CTabItem getDraggedTabItem() {
+ return draggedTabItem;
+ }
+
@Override
public CTabFolder resolveDropFolderForZone(CTabFolder targetFolder, int
zone) {
// Only the docked layout tree supports splitting; detached windows stay
single-pane in v1.