Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ image:hop-gui/popup-collapsed.png[Hop Gui Popup Dialog - Collapsed, width="65%"]

image:hop-gui/popup-no-categories-no-fixed-width.png[Hop Gui Popup Dialog - No categories, No fixed width, width="65%"]

== Adding transforms and actions

When you open the popup dialog from a **pipeline** or **workflow** canvas (single-click on the background), the dialog lists every transform or action you can add.

* **Click** a transform or action to place it at the canvas location where you opened the dialog.
* **Click-drag** a transform or action from the dialog onto the canvas to place it where you release the mouse. In Hop Gui the icon follows the pointer; in Hop Web use drag-and-drop onto the canvas. Release outside the canvas (or press Esc in Hop Gui) to cancel.
* **ALT-Click** (Option-Click on macOS) a transform or action to add or remove it as a *favorite* without closing the dialog. Favorites appear in their own category near the top of the list for quicker access.

Hover over an item to see its description and these shortcuts in the tooltip.

TIP: Dropping a new transform or action on top of an existing hop can split that hop (same confirmation dialog as when you move an icon onto a hop).

== Creating Items

When you create a new item, the dialog will show you a list of metadata items that can be created with a single click of a button.
Expand Down
239 changes: 237 additions & 2 deletions ui/src/main/java/org/apache/hop/ui/core/dialog/ContextDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,18 @@
import org.apache.hop.ui.core.gui.WindowProperty;
import org.apache.hop.ui.core.widget.OsHelper;
import org.apache.hop.ui.hopgui.ToolbarFacade;
import org.apache.hop.ui.hopgui.context.ContextDialogPlacement;
import org.apache.hop.ui.hopgui.context.GuiActionFavorites;
import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceAdapter;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
Expand All @@ -72,6 +79,7 @@
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Shell;
Expand Down Expand Up @@ -118,6 +126,34 @@ public class ContextDialog extends Dialog {
private boolean ctrlClicked;
private boolean focusLost;

/**
* True when the user started dragging a placeable Create item out of this dialog (issue #3111).
* On native SWT the dialog closes on drag-start and the graph continues placement; on Hop Web
* HTML5/SWT DnD is used and the shell is only hidden until dragFinished.
*/
private boolean placementDrag;

/**
* True when a canvas DropTarget already created the transform/action (Hop Web DnD path). Prevents
* GuiContextUtil from starting a second placement gesture.
*/
private boolean placementCompletedByDrop;

/** Item under the mouse when a potential placement drag was armed (MouseDown on Create item). */
private Item pressItem;

/** Display coordinates of the MouseDown that armed a potential placement drag. */
private org.eclipse.swt.graphics.Point pressDisplayLocation;

private Listener placementArmMoveFilter;
private Listener placementArmUpFilter;

/** Item currently being dragged via SWT DnD (Hop Web). */
private Item dndDragItem;

/** Minimum pointer movement (display px) before a press on a Create item becomes a drag. */
private static final int PLACEMENT_DRAG_THRESHOLD_PX = 8;

/** All context items. */
private final List<Item> items = new ArrayList<>();

Expand Down Expand Up @@ -502,9 +538,14 @@ public GuiAction open() {

wCanvas.addListener(SWT.KeyDown, this::onKeyPressed);
wCanvas.addListener(SWT.Paint, this::onPaint);
wCanvas.addListener(SWT.MouseDown, this::onMouseDown);
wCanvas.addListener(SWT.MouseUp, this::onMouseUp);
if (!EnvironmentUtils.getInstance().isWeb()) {
wCanvas.addListener(SWT.MouseMove, this::onMouseMove);
} else {
// Hop Web: RAP does not deliver reliable mouse-move-while-pressed for Display filters.
// Use HTML5-backed SWT DnD so the user can drag a create item onto the graph canvas.
installWebPlacementDragSource();
}

// OS Specific listeners...
Expand Down Expand Up @@ -651,6 +692,11 @@ public boolean isDisposed() {
}

public void dispose() {
if (shell == null || shell.isDisposed()) {
return;
}

removePlacementArmFilters();

// Store the toolbar settings
storeDialogSettings();
Expand All @@ -663,8 +709,12 @@ public void dispose() {
// There's no need to keep re-loading all the time.
// Previously this cache was not functional so that we needed to dispose here.

highlightColor.dispose();
headerFont.dispose();
if (highlightColor != null && !highlightColor.isDisposed()) {
highlightColor.dispose();
}
if (headerFont != null && !headerFont.isDisposed()) {
headerFont.dispose();
}
}

@GuiToolbarElement(
Expand Down Expand Up @@ -744,7 +794,38 @@ private void onMouseMove(Event event) {
}
}

private void onMouseDown(Event event) {
if (event.button != 1 || placementDrag) {
return;
}
AreaOwner areaOwner = AreaOwner.getVisibleAreaOwner(areaOwners, event.x, event.y);
if (areaOwner == null || areaOwner.getParent() != OwnerType.ITEM) {
return;
}
Item item = (Item) areaOwner.getOwner();
if (item == null || !GuiActionFavorites.isPlaceableCreateAction(item.getAction())) {
return;
}
selectItem(item, false);
// Native Hop GUI: arm Display-filter placement drag. Hop Web uses SWT DnD instead (see
// installWebPlacementDragSource) because RAP does not deliver mouse-move-while-pressed.
if (EnvironmentUtils.getInstance().isWeb()) {
return;
}
pressItem = item;
pressDisplayLocation = shell.getDisplay().getCursorLocation();
installPlacementArmFilters();
}

private void onMouseUp(Event event) {
if (placementDrag) {
// Drag already committed; dialog is closing or closed.
return;
}
removePlacementArmFilters();
pressItem = null;
pressDisplayLocation = null;

AreaOwner areaOwner = AreaOwner.getVisibleAreaOwner(areaOwners, event.x, event.y);
if (areaOwner == null) {
return;
Expand Down Expand Up @@ -796,6 +877,148 @@ private void onMouseUp(Event event) {
}
}

private void installPlacementArmFilters() {
removePlacementArmFilters();
Display display = shell.getDisplay();
placementArmMoveFilter =
event -> {
if (event.type != SWT.MouseMove || pressItem == null || placementDrag) {
return;
}
// Only commit while the primary button is still held (avoids stray move events).
if ((event.stateMask & SWT.BUTTON1) == 0) {
return;
}
if (shell.isDisposed()) {
removePlacementArmFilters();
return;
}
org.eclipse.swt.graphics.Point cursor = display.getCursorLocation();
int dx = cursor.x - pressDisplayLocation.x;
int dy = cursor.y - pressDisplayLocation.y;
int thresholdSq = PLACEMENT_DRAG_THRESHOLD_PX * PLACEMENT_DRAG_THRESHOLD_PX;
if (dx * dx + dy * dy > thresholdSq) {
commitPlacementDrag(pressItem);
}
};
placementArmUpFilter =
event -> {
if (event.type == SWT.MouseUp) {
// Click path: dialog MouseUp will select. Clear arm state only.
removePlacementArmFilters();
pressItem = null;
pressDisplayLocation = null;
}
};
display.addFilter(SWT.MouseMove, placementArmMoveFilter);
display.addFilter(SWT.MouseUp, placementArmUpFilter);
}

private void removePlacementArmFilters() {
if (shell == null || shell.isDisposed()) {
placementArmMoveFilter = null;
placementArmUpFilter = null;
return;
}
Display display = shell.getDisplay();
if (placementArmMoveFilter != null) {
display.removeFilter(SWT.MouseMove, placementArmMoveFilter);
placementArmMoveFilter = null;
}
if (placementArmUpFilter != null) {
display.removeFilter(SWT.MouseUp, placementArmUpFilter);
placementArmUpFilter = null;
}
}

private void commitPlacementDrag(Item item) {
if (item == null || placementDrag) {
return;
}
selectedAction = item.getAction();
placementDrag = true;
focusLost = false;
shiftClicked = false;
ctrlClicked = false;
pressItem = null;
pressDisplayLocation = null;
removePlacementArmFilters();
dispose();
}

/**
* Hop Web: DragSource on the icon canvas so HTML5 DnD can carry a placeable create action to the
* pipeline/workflow canvas DropTarget. The shell is hidden on dragStart (so the canvas is
* visible) but kept alive until dragFinished so the DragSource remains valid.
*/
private void installWebPlacementDragSource() {
DragSource dragSource = new DragSource(wCanvas, DND.DROP_COPY);
dragSource.setTransfer(new Transfer[] {TextTransfer.getInstance()});
dragSource.addDragListener(
new DragSourceAdapter() {
@Override
public void dragStart(DragSourceEvent event) {
Item item = findItem(event.x, event.y);
if (item == null || !GuiActionFavorites.isPlaceableCreateAction(item.getAction())) {
event.doit = false;
dndDragItem = null;
return;
}
dndDragItem = item;
selectItem(item, false);
selectedAction = item.getAction();
placementDrag = true;
placementCompletedByDrop = false;
focusLost = false;
// Prefer the item icon as drag image; fall back to Hop logo on web if needed.
if (item.getImage() != null && !item.getImage().isDisposed()) {
event.image = item.getImage();
} else {
event.image = GuiResource.getInstance().getImageHop();
}
// Hide (do not dispose) so the graph canvas is usable while the DragSource stays alive.
if (shell != null && !shell.isDisposed()) {
shell.setVisible(false);
}
event.doit = true;
}

@Override
public void dragSetData(DragSourceEvent event) {
if (TextTransfer.getInstance().isSupportedType(event.dataType) && dndDragItem != null) {
event.data = ContextDialogPlacement.encode(dndDragItem.getAction());
event.doit = event.data != null;
}
}

@Override
public void dragFinished(DragSourceEvent event) {
dndDragItem = null;
// End the modal open() loop. If the drop already created the item,
// GuiContextUtil will see placementCompletedByDrop and skip a second create.
if (selectedAction == null && !placementCompletedByDrop) {
// Drag cancelled without a selection — treat as focus-lost style cancel.
placementDrag = false;
}
dispose();
}
});
}

/** Called by canvas drop targets when a web DnD drop successfully placed a transform/action. */
public void markPlacementCompletedByDrop() {
placementCompletedByDrop = true;
placementDrag = true;
focusLost = false;
}

/**
* @return true if a canvas DropTarget already handled creation for this placement gesture
*/
public boolean isPlacementCompletedByDrop() {
return placementCompletedByDrop;
}

/**
* Rebuild the category list and icon items from the current {@link #actions} list. Preserves
* collapsed state of categories when refreshing after a favorites toggle.
Expand Down Expand Up @@ -1228,10 +1451,22 @@ else if (!filteredItems.contains(selectedItem)) {
}

private void onFocusLost() {
// Placement drag closes the dialog intentionally; do not treat as cancel.
if (placementDrag || selectedAction != null) {
return;
}
focusLost = true;
dispose();
}

/**
* @return true if the dialog closed because the user started dragging a placeable create item
* onto the canvas (issue #3111)
*/
public boolean isPlacementDrag() {
return placementDrag;
}

private void onModifySearch() {
String text = wSearch.getText();
this.filter(text);
Expand Down
Loading
Loading