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 9abda5d915 [Bug]: Ctrl+C, Ctrl+V and Ctrl+X fail for graph elements in 
Hop Web (#7681)
9abda5d915 is described below

commit 9abda5d915ea713bb9af4f42a08525074aac64a1
Author: Michael <[email protected]>
AuthorDate: Mon Aug 3 01:59:03 2026 -0500

    [Bug]: Ctrl+C, Ctrl+V and Ctrl+X fail for graph elements in Hop Web (#7681)
    
    * Fix Hop Web clipboard operations
    
    * Delete PR_HOP_WEB_CLIPBOARD.md
    
    ---------
    
    Co-authored-by: Michael <michaaels>
---
 .../hop/ui/hopgui/ContentEditorFacadeImpl.java     | 24 +++++++++
 .../org/apache/hop/ui/hopgui/HopWebEntryPoint.java | 19 +++++--
 .../org/apache/hop/ui/hopgui/monaco-editor.js      | 19 ++++++-
 .../apache/hop/ui/hopgui/HopWebEntryPointTest.java | 58 ++++++++++++++++++++++
 .../org/apache/hop/ui/core/gui/GuiResource.java    | 19 +++++++
 .../org/apache/hop/ui/core/gui/WebClipboard.java   | 35 +++++++++++++
 .../main/java/org/apache/hop/ui/hopgui/HopGui.java | 46 ++++++-----------
 .../hop/ui/hopgui/TextEditingControlUtil.java      | 43 ++++++++++++++++
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  |  6 ++-
 .../delegates/HopGuiPipelineClipboardDelegate.java | 11 ++--
 .../hopgui/file/workflow/HopGuiWorkflowGraph.java  |  6 ++-
 .../delegates/HopGuiWorkflowClipboardDelegate.java | 11 ++--
 .../apache/hop/ui/core/gui/WebClipboardTest.java   | 40 +++++++++++++++
 .../hop/ui/hopgui/TextEditingControlUtilTest.java  | 44 ++++++++++++++++
 14 files changed, 332 insertions(+), 49 deletions(-)

diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/ContentEditorFacadeImpl.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/ContentEditorFacadeImpl.java
index dd6aac7a6f..6d02102e1f 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/ContentEditorFacadeImpl.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/ContentEditorFacadeImpl.java
@@ -80,9 +80,11 @@ public class ContentEditorFacadeImpl extends 
ContentEditorFacade {
 
       remoteObject.setHandler(widget.getOperationHandler());
       remoteObject.listen("contentChanged", true);
+      remoteObject.listen("focusChanged", true);
       host.addListener(
           SWT.Dispose,
           event -> {
+            widget.clearFocus();
             try {
               remoteObject.destroy();
             } catch (Exception ignored) {
@@ -142,6 +144,7 @@ public class ContentEditorFacadeImpl extends 
ContentEditorFacade {
 
     private final Composite root;
     private final RemoteObject remoteObject;
+    private final Display display;
     private volatile String cachedContent = "";
     private final java.util.List<ModifyListener> modifyListeners = new 
CopyOnWriteArrayList<>();
     private boolean suppressModify;
@@ -152,11 +155,16 @@ public class ContentEditorFacadeImpl extends 
ContentEditorFacade {
         Composite root, Composite host, RemoteObject remoteObject, String 
languageId) {
       this.root = root;
       this.remoteObject = remoteObject;
+      this.display = host.getDisplay();
       this.languageId = languageId != null ? languageId : "";
       this.operationHandler =
           new AbstractOperationHandler() {
             @Override
             public void handleNotify(String event, JsonObject properties) {
+              if ("focusChanged".equals(event) && properties.get("focused") != 
null) {
+                setFocus(properties.get("focused").asBoolean());
+                return;
+              }
               if (!"contentChanged".equals(event) || properties.get("content") 
== null) {
                 return;
               }
@@ -189,6 +197,22 @@ public class ContentEditorFacadeImpl extends 
ContentEditorFacade {
           };
     }
 
+    private void setFocus(boolean focused) {
+      if (display == null || display.isDisposed()) {
+        return;
+      }
+      Object focusedEditor = display.getData(HopGui.TEXT_EDITOR_FOCUS_DATA);
+      if (focused) {
+        display.setData(HopGui.TEXT_EDITOR_FOCUS_DATA, this);
+      } else if (focusedEditor == this) {
+        display.setData(HopGui.TEXT_EDITOR_FOCUS_DATA, null);
+      }
+    }
+
+    void clearFocus() {
+      setFocus(false);
+    }
+
     AbstractOperationHandler getOperationHandler() {
       return operationHandler;
     }
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 2119120da0..8177382d19 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
@@ -18,6 +18,7 @@
 package org.apache.hop.ui.hopgui;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -50,6 +51,9 @@ import org.eclipse.swt.widgets.Display;
 
 public class HopWebEntryPoint extends AbstractEntryPoint {
 
+  private static final Set<String> NATIVE_TEXT_EDITING_SHORTCUTS =
+      Set.of("CTRL+C", "CTRL+V", "CTRL+X");
+
   /** Audit group/type/name for Hop Web theme preference (per-user in audit 
folder). */
   public static final String AUDIT_GROUP_HOP_WEB = "hop-web";
 
@@ -129,9 +133,9 @@ public class HopWebEntryPoint extends AbstractEntryPoint {
     // CANCEL_KEYS prevents the browser from handling these shortcuts
     // Note: CTRL automatically maps to Command key on Mac
     Display display = parent.getDisplay();
-    String[] allShortcuts = buildKeyboardShortcuts();
-    display.setData(RWT.ACTIVE_KEYS, allShortcuts);
-    display.setData(RWT.CANCEL_KEYS, allShortcuts);
+    String[] activeShortcuts = buildKeyboardShortcuts();
+    display.setData(RWT.ACTIVE_KEYS, activeShortcuts);
+    display.setData(RWT.CANCEL_KEYS, 
buildCancelledKeyboardShortcuts(activeShortcuts));
 
     // Transferring Widget Data for client-side canvas drawing instructions
     WidgetUtil.registerDataKeys("props");
@@ -339,6 +343,13 @@ public class HopWebEntryPoint extends AbstractEntryPoint {
     return shortcuts.toArray(new String[0]);
   }
 
+  static String[] buildCancelledKeyboardShortcuts(String[] activeShortcuts) {
+    return Arrays.stream(activeShortcuts)
+        .filter(shortcut -> !NATIVE_TEXT_EDITING_SHORTCUTS.contains(shortcut))
+        .distinct()
+        .toArray(String[]::new);
+  }
+
   /**
    * Convert a KeyboardShortcut to RAP format for ACTIVE_KEYS / CANCEL_KEYS. 
RAP only supports CTRL,
    * ALT, SHIFT (not META), so we use CTRL+ for all command/control shortcuts; 
on Mac the browser
@@ -347,7 +358,7 @@ public class HopWebEntryPoint extends AbstractEntryPoint {
    * @param shortcut The keyboard shortcut to convert
    * @return RAP format string (e.g., "CTRL+C", "ALT+SHIFT+F1") or null if 
invalid
    */
-  private String convertToRapFormat(KeyboardShortcut shortcut) {
+  String convertToRapFormat(KeyboardShortcut shortcut) {
     if (shortcut.getKeyCode() == 0) {
       return null;
     }
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/monaco-editor.js 
b/rap/src/main/resources/org/apache/hop/ui/hopgui/monaco-editor.js
index 9c463327f2..3393a71772 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/monaco-editor.js
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/monaco-editor.js
@@ -75,6 +75,7 @@
       return new hop.MonacoEditor(properties);
     },
     destructor: function(widget) {
+      widget._notifyFocus(false);
       widget._destroyed = true;
       if (widget._retryId) { clearInterval(widget._retryId); widget._retryId = 
null; }
       if (widget._debounceId) { clearTimeout(widget._debounceId); 
widget._debounceId = null; }
@@ -87,7 +88,7 @@
       }
     },
     properties: [ "content", "language", "readOnly", "theme" ],
-    events: [ "contentChanged" ]
+    events: [ "contentChanged", "focusChanged" ]
   });
 
   rwt.define("hop");
@@ -162,6 +163,17 @@
       }
     },
 
+    _notifyFocus: function(focused) {
+      try {
+        var remote = rap.getRemoteObject(this);
+        if (remote) {
+          remote.notify("focusChanged", { focused: focused });
+        }
+      } catch (e) {
+        console.warn("MonacoEditor: failed to notify focus", e);
+      }
+    },
+
     _scheduleNotify: function() {
       var self = this;
       if (self._debounceId) clearTimeout(self._debounceId);
@@ -229,7 +241,12 @@
           self._scheduleNotify();
         });
 
+        self._editor.onDidFocusEditorWidget(function() {
+          self._notifyFocus(true);
+        });
+
         self._editor.onDidBlurEditorWidget(function() {
+          self._notifyFocus(false);
           if (self._debounceId) {
             clearTimeout(self._debounceId);
             self._debounceId = null;
diff --git 
a/rap/src/test/java/org/apache/hop/ui/hopgui/HopWebEntryPointTest.java 
b/rap/src/test/java/org/apache/hop/ui/hopgui/HopWebEntryPointTest.java
new file mode 100644
index 0000000000..decef4fb17
--- /dev/null
+++ b/rap/src/test/java/org/apache/hop/ui/hopgui/HopWebEntryPointTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import org.apache.hop.core.gui.plugin.key.KeyboardShortcut;
+import org.junit.jupiter.api.Test;
+
+class HopWebEntryPointTest {
+
+  @Test
+  void preservesNativeTextEditingShortcutsAsActiveOnly() {
+    String[] activeShortcuts = {"CTRL+C", "CTRL+V", "CTRL+X", "CTRL+S"};
+
+    String[] cancelledShortcuts = 
HopWebEntryPoint.buildCancelledKeyboardShortcuts(activeShortcuts);
+
+    assertArrayEquals(new String[] {"CTRL+C", "CTRL+V", "CTRL+X", "CTRL+S"}, 
activeShortcuts);
+    assertArrayEquals(new String[] {"CTRL+S"}, cancelledShortcuts);
+  }
+
+  @Test
+  void removesDuplicateCancelledShortcuts() {
+    String[] cancelledShortcuts =
+        HopWebEntryPoint.buildCancelledKeyboardShortcuts(
+            new String[] {"CTRL+S", "CTRL+C", "CTRL+S"});
+
+    assertEquals(cancelledShortcuts.length, 
Arrays.stream(cancelledShortcuts).distinct().count());
+  }
+
+  @Test
+  void mapsMacCommandShortcutToRapControlShortcut() {
+    KeyboardShortcut shortcut = mock(KeyboardShortcut.class);
+    when(shortcut.getKeyCode()).thenReturn((int) 'c');
+    when(shortcut.isCommand()).thenReturn(true);
+
+    assertEquals("CTRL+C", new 
HopWebEntryPoint().convertToRapFormat(shortcut));
+  }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
index 919134384a..3e155ca999 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
@@ -39,8 +39,11 @@ import org.apache.hop.core.util.Utils;
 import org.apache.hop.ui.core.ConstUi;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.widget.OsHelper;
+import org.apache.hop.ui.hopgui.HopWebUrlHelper;
+import org.apache.hop.ui.hopgui.IHopWebUrlUpdater;
 import org.apache.hop.ui.hopgui.ISingletonProvider;
 import org.apache.hop.ui.hopgui.ImplementationLoader;
+import org.apache.hop.ui.util.EnvironmentUtils;
 import org.apache.hop.ui.util.SwtSvgImageUtil;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.dnd.Clipboard;
@@ -276,6 +279,8 @@ public class GuiResource {
    */
   private Clipboard clipboard;
 
+  private final WebClipboard webClipboard = new WebClipboard();
+
   protected GuiResource() {
     this(Display.getCurrent());
   }
@@ -1162,12 +1167,26 @@ public class GuiResource {
       return;
     }
 
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      IHopWebUrlUpdater urlUpdater = HopWebUrlHelper.getUrlUpdater();
+      try {
+        webClipboard.write(cliptext, urlUpdater == null ? null : 
urlUpdater::copyToClipboard);
+      } catch (Exception e) {
+        log.logDebug("Unable to copy text to the browser clipboard", e);
+      }
+      return;
+    }
+
     getNewClipboard();
     TextTransfer tran = TextTransfer.getInstance();
     clipboard.setContents(new String[] {cliptext}, new Transfer[] {tran});
   }
 
   public String fromClipboard() {
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      return webClipboard.read();
+    }
+
     getNewClipboard();
     TextTransfer tran = TextTransfer.getInstance();
 
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/WebClipboard.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/WebClipboard.java
new file mode 100644
index 0000000000..a7c4cb49f0
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/WebClipboard.java
@@ -0,0 +1,35 @@
+/*
+ * 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.core.gui;
+
+import java.util.function.Consumer;
+
+final class WebClipboard {
+
+  private String text;
+
+  String read() {
+    return text;
+  }
+
+  void write(String text, Consumer<String> browserWriter) {
+    this.text = text;
+    if (browserWriter != null) {
+      browserWriter.accept(text);
+    }
+  }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index db68c595f8..c58f872451 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -21,7 +21,6 @@ import static org.apache.hop.core.Const.getDocUrl;
 
 import java.io.FileOutputStream;
 import java.io.PrintStream;
-import java.lang.reflect.Method;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -164,6 +163,8 @@ public class HopGui
     implements IActionContextHandlersProvider, ISearchableProvider, 
IHasHopMetadataProvider {
   private static final Class<?> PKG = HopGui.class;
 
+  public static final String TEXT_EDITOR_FOCUS_DATA = HopGui.class.getName() + 
".textEditorFocus";
+
   // The main Menu IDs
   public static final String ID_MAIN_MENU = "HopGui-Menu";
   public static final String ID_MAIN_MENU_FILE = "10000-menu-file";
@@ -1318,11 +1319,9 @@ public class HopGui
   @GuiKeyboardShortcut(control = true, key = 'c')
   @GuiOsxKeyboardShortcut(command = true, key = 'c')
   public void menuEditCopySelected() {
-    Control focusControl = display.getFocusControl();
-    if (isStyledTextControl(focusControl)) {
-      if (getStyledTextSelectionCount(focusControl) > 0) {
-        return;
-      }
+    if (TextEditingControlUtil.isTextEditingControl(display.getFocusControl())
+        || isWebTextEditorFocused()) {
+      return;
     }
 
     // Otherwise, delegate to the active file type handler (pipeline/workflow)
@@ -1338,9 +1337,8 @@ public class HopGui
   @GuiKeyboardShortcut(control = true, key = 'v')
   @GuiOsxKeyboardShortcut(command = true, key = 'v')
   public void menuEditPaste() {
-    Control focusControl = display.getFocusControl();
-    if (isStyledTextControl(focusControl)) {
-      // Terminal handles paste internally
+    if (TextEditingControlUtil.isTextEditingControl(display.getFocusControl())
+        || isWebTextEditorFocused()) {
       return;
     }
 
@@ -1357,32 +1355,16 @@ public class HopGui
   @GuiKeyboardShortcut(control = true, key = 'x')
   @GuiOsxKeyboardShortcut(command = true, key = 'x')
   public void menuEditCutSelected() {
-    getActiveFileTypeHandler().cutSelectedToClipboard();
-  }
+    if (TextEditingControlUtil.isTextEditingControl(display.getFocusControl())
+        || isWebTextEditorFocused()) {
+      return;
+    }
 
-  /**
-   * Returns true if the control is a StyledText. Uses class name so RAP (Hop 
Web) does not load
-   * org.eclipse.swt.custom.StyledText, which is not available in RAP.
-   */
-  private static boolean isStyledTextControl(Control control) {
-    return control != null
-        && 
"org.eclipse.swt.custom.StyledText".equals(control.getClass().getName());
+    getActiveFileTypeHandler().cutSelectedToClipboard();
   }
 
-  /**
-   * Returns getSelectionCount() for a StyledText control, or 0. Uses 
reflection so StyledText is
-   * not loaded on RAP.
-   */
-  private static int getStyledTextSelectionCount(Control control) {
-    if (!isStyledTextControl(control)) {
-      return 0;
-    }
-    try {
-      Method m = control.getClass().getMethod("getSelectionCount");
-      return ((Number) m.invoke(control)).intValue();
-    } catch (Exception e) {
-      return 0;
-    }
+  private boolean isWebTextEditorFocused() {
+    return display.getData(TEXT_EDITOR_FOCUS_DATA) != null;
   }
 
   @GuiMenuElement(
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/TextEditingControlUtil.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/TextEditingControlUtil.java
new file mode 100644
index 0000000000..580eab8466
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/TextEditingControlUtil.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+
+final class TextEditingControlUtil {
+
+  private static final String STYLED_TEXT_CLASS = 
"org.eclipse.swt.custom.StyledText";
+
+  private TextEditingControlUtil() {}
+
+  /** Uses the class name for StyledText so RAP does not load the desktop-only 
StyledText class. */
+  static boolean isTextEditingControl(Control control) {
+    if (control instanceof Text) {
+      return true;
+    }
+    for (Class<?> type = control == null ? null : control.getClass();
+        type != null;
+        type = type.getSuperclass()) {
+      if (STYLED_TEXT_CLASS.equals(type.getName())) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index bae08e57f9..bd8060090f 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -6095,8 +6095,10 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   @GuiOsxKeyboardShortcut(command = true, key = 'x')
   @Override
   public void cutSelectedToClipboard() {
-    pipelineClipboardDelegate.copySelected(
-        pipelineMeta, pipelineMeta.getSelectedTransforms(), 
pipelineMeta.getSelectedNotes());
+    if (!pipelineClipboardDelegate.copySelected(
+        pipelineMeta, pipelineMeta.getSelectedTransforms(), 
pipelineMeta.getSelectedNotes())) {
+      return;
+    }
     pipelineTransformDelegate.delTransforms(pipelineMeta, 
pipelineMeta.getSelectedTransforms());
     notePadDelegate.deleteNotes(pipelineMeta, pipelineMeta.getSelectedNotes());
   }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineClipboardDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineClipboardDelegate.java
index 062c8f5c8f..8cff22ab73 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineClipboardDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineClipboardDelegate.java
@@ -64,15 +64,17 @@ public class HopGuiPipelineClipboardDelegate {
     ILogChannel log = hopGui.getLog();
   }
 
-  public void toClipboard(String clipText) {
+  public boolean toClipboard(String clipText) {
     try {
       GuiResource.getInstance().toClipboard(clipText);
+      return true;
     } catch (Exception e) {
       new ErrorDialog(
           hopGui.getActiveShell(),
           BaseMessages.getString(PKG, 
"HopGui.Dialog.ExceptionCopyToClipboard.Title"),
           BaseMessages.getString(PKG, 
"HopGui.Dialog.ExceptionCopyToClipboard.Message"),
           e);
+      return false;
     }
   }
 
@@ -298,10 +300,10 @@ public class HopGuiPipelineClipboardDelegate {
     location.y = location.y + (int) (5 * 
PropsUi.getInstance().getZoomFactor());
   }
 
-  public void copySelected(
+  public boolean copySelected(
       PipelineMeta pipelineMeta, List<TransformMeta> transforms, 
List<NotePadMeta> notes) {
     if (transforms == null || transforms.isEmpty() && notes.isEmpty()) {
-      return;
+      return false;
     }
 
     StringBuilder xml = new 
StringBuilder(5000).append(XmlHandler.getXmlHeader());
@@ -313,9 +315,10 @@ public class HopGuiPipelineClipboardDelegate {
       serializeTransformErrorHandlingToXml(transforms, xml);
       
xml.append(XmlHandler.closeTag(XML_TAG_PIPELINE_TRANSFORMS)).append(Const.CR);
 
-      toClipboard(xml.toString());
+      return toClipboard(xml.toString());
     } catch (Exception ex) {
       new ErrorDialog(hopGui.getActiveShell(), "Error", "Error encoding to 
XML", ex);
+      return false;
     }
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index d49fbf1f39..bf15ca081b 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -2473,8 +2473,10 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
   @GuiOsxKeyboardShortcut(command = true, key = 'x')
   @Override
   public void cutSelectedToClipboard() {
-    workflowClipboardDelegate.copySelected(
-        workflowMeta, workflowMeta.getSelectedActions(), 
workflowMeta.getSelectedNotes());
+    if (!workflowClipboardDelegate.copySelected(
+        workflowMeta, workflowMeta.getSelectedActions(), 
workflowMeta.getSelectedNotes())) {
+      return;
+    }
     deleteSelected();
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowClipboardDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowClipboardDelegate.java
index a6990d870d..833f1af46c 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowClipboardDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowClipboardDelegate.java
@@ -62,15 +62,17 @@ public class HopGuiWorkflowClipboardDelegate {
     this.log = hopGui.getLog();
   }
 
-  public void toClipboard(String clipText) {
+  public boolean toClipboard(String clipText) {
     try {
       GuiResource.getInstance().toClipboard(clipText);
+      return true;
     } catch (Throwable e) {
       new ErrorDialog(
           hopGui.getActiveShell(),
           BaseMessages.getString(PKG, 
"HopGui.Dialog.ExceptionCopyToClipboard.Title"),
           BaseMessages.getString(PKG, 
"HopGui.Dialog.ExceptionCopyToClipboard.Message"),
           e);
+      return false;
     }
   }
 
@@ -300,10 +302,10 @@ public class HopGuiWorkflowClipboardDelegate {
     return uniqueName;
   }
 
-  public void copySelected(
+  public boolean copySelected(
       WorkflowMeta workflowMeta, List<ActionMeta> actions, List<NotePadMeta> 
notes) {
     if (actions == null || actions.isEmpty() && notes.isEmpty()) {
-      return;
+      return false;
     }
 
     StringBuilder xml = new 
StringBuilder(5000).append(XmlHandler.getXmlHeader());
@@ -341,9 +343,10 @@ public class HopGuiWorkflowClipboardDelegate {
 
       
xml.append(XmlHandler.closeTag(XML_TAG_WORKFLOW_ACTIONS)).append(Const.CR);
 
-      toClipboard(xml.toString());
+      return toClipboard(xml.toString());
     } catch (Exception ex) {
       new ErrorDialog(hopGui.getActiveShell(), "Error", "Error encoding to 
XML", ex);
+      return false;
     }
   }
 
diff --git a/ui/src/test/java/org/apache/hop/ui/core/gui/WebClipboardTest.java 
b/ui/src/test/java/org/apache/hop/ui/core/gui/WebClipboardTest.java
new file mode 100644
index 0000000000..fbe97c204e
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/core/gui/WebClipboardTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.core.gui;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.Test;
+
+class WebClipboardTest {
+
+  @Test
+  void storesTextPerInstanceAndForwardsItToTheBrowserWriter() {
+    WebClipboard first = new WebClipboard();
+    WebClipboard second = new WebClipboard();
+    AtomicReference<String> browserText = new AtomicReference<>();
+
+    assertNull(first.read());
+    first.write("<pipeline-transforms/>", browserText::set);
+
+    assertEquals("<pipeline-transforms/>", first.read());
+    assertEquals("<pipeline-transforms/>", browserText.get());
+    assertNull(second.read());
+  }
+}
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/TextEditingControlUtilTest.java 
b/ui/src/test/java/org/apache/hop/ui/hopgui/TextEditingControlUtilTest.java
new file mode 100644
index 0000000000..e6a40f6325
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/hopgui/TextEditingControlUtilTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.junit.jupiter.api.Test;
+
+class TextEditingControlUtilTest {
+
+  @Test
+  void recognizesTextEditingControls() {
+    assertTrue(TextEditingControlUtil.isTextEditingControl(mock(Text.class)));
+    
assertTrue(TextEditingControlUtil.isTextEditingControl(mock(StyledText.class)));
+  }
+
+  @Test
+  void rejectsNonTextControls() {
+    
assertFalse(TextEditingControlUtil.isTextEditingControl(mock(Canvas.class)));
+    
assertFalse(TextEditingControlUtil.isTextEditingControl(mock(Composite.class)));
+    assertFalse(TextEditingControlUtil.isTextEditingControl(null));
+  }
+}

Reply via email to