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 8177382d19..352b48f105 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 @@ -51,9 +51,31 @@ public class HopWebEntryPoint extends AbstractEntryPoint { + /** + * Shortcuts that must remain active (sent to the server) but must not be cancelled in the + * browser, so native text editing still works (e.g. Ctrl+C/V/X in input fields). + */ private static final Set NATIVE_TEXT_EDITING_SHORTCUTS = Set.of("CTRL+C", "CTRL+V", "CTRL+X"); + /** + * Bare navigation keys used for caret movement and selection in text fields (and tree widgets). + * They may stay in {@code ACTIVE_KEYS} so canvas pan shortcuts still reach the server when focus + * is on the graph, but must not be in {@code CANCEL_KEYS} or the browser never moves the caret + * (see issue #7833). Modifier combinations (CTRL+ARROW_*, ALT+ARROW_*, …) are separate RAP keys + * and remain cancelled when registered as application shortcuts. + */ + private static final Set NATIVE_TEXT_NAVIGATION_KEYS = + Set.of( + "ARROW_UP", + "ARROW_DOWN", + "ARROW_LEFT", + "ARROW_RIGHT", + "HOME", + "END", + "PAGE_UP", + "PAGE_DOWN"); + /** Audit group/type/name for Hop Web theme preference (per-user in audit folder). */ public static final String AUDIT_GROUP_HOP_WEB = "hop-web"; @@ -346,6 +368,7 @@ private String[] buildKeyboardShortcuts() { static String[] buildCancelledKeyboardShortcuts(String[] activeShortcuts) { return Arrays.stream(activeShortcuts) .filter(shortcut -> !NATIVE_TEXT_EDITING_SHORTCUTS.contains(shortcut)) + .filter(shortcut -> !NATIVE_TEXT_NAVIGATION_KEYS.contains(shortcut)) .distinct() .toArray(String[]::new); } 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 index decef4fb17..60c65c1c7e 100644 --- a/rap/src/test/java/org/apache/hop/ui/hopgui/HopWebEntryPointTest.java +++ b/rap/src/test/java/org/apache/hop/ui/hopgui/HopWebEntryPointTest.java @@ -38,6 +38,28 @@ void preservesNativeTextEditingShortcutsAsActiveOnly() { assertArrayEquals(new String[] {"CTRL+S"}, cancelledShortcuts); } + @Test + void preservesBareNavigationKeysAsActiveOnly() { + // Canvas pan registers bare arrows/HOME as ACTIVE_KEYS; CANCEL would block caret movement in + // text fields (issue #7833). Modifier combos stay cancellable for app shortcuts. + String[] activeShortcuts = { + "ARROW_LEFT", + "ARROW_RIGHT", + "ARROW_UP", + "ARROW_DOWN", + "HOME", + "END", + "PAGE_UP", + "PAGE_DOWN", + "CTRL+ARROW_LEFT", + "CTRL+S" + }; + + String[] cancelledShortcuts = HopWebEntryPoint.buildCancelledKeyboardShortcuts(activeShortcuts); + + assertArrayEquals(new String[] {"CTRL+ARROW_LEFT", "CTRL+S"}, cancelledShortcuts); + } + @Test void removesDuplicateCancelledShortcuts() { String[] cancelledShortcuts = diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java index 673a64d24e..9d2d467c75 100644 --- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java +++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java @@ -83,23 +83,13 @@ public void keyPressed(KeyEvent event) { return; } - // Ignore shortcuts inside Text, Combo, StyledText, or CCombo widgets (including terminal). + // Do not steal keys needed for native editing / caret movement inside text-like widgets. // StyledText is not available in RAP, so we check via reflection to avoid NoClassDefFoundError. - if (event.widget instanceof Text - || event.widget instanceof Combo - || event.widget instanceof CCombo - || isStyledText(event.widget)) { - // Ignore Copy/Cut/Paste/Select all - check both keyCode and character - if ((event.stateMask & (SWT.CONTROL + SWT.COMMAND)) != 0) { - char key = Character.toLowerCase((char) event.keyCode); - if (key == 'a' || key == 'c' || key == 'v' || key == 'x') { - return; - } - } - // Ignore DEL and Backspace - if (event.keyCode == SWT.DEL || event.character == SWT.BS) { - return; - } + // Bare ARROW_*/HOME/END would otherwise match canvas pan shortcuts (DragViewZoomBase) and break + // caret navigation — especially in Hop Web where RAP CANCEL_KEYS can also block the browser + // (see issue #7833). App shortcuts with CTRL/CMD/ALT (e.g. Ctrl+S, Ctrl+Arrow) still run. + if (isTextLikeWidget(event.widget) && isNativeTextEditingKey(event)) { + return; } List orderedParents = getParentObjectsInContextOrder(event.widget); @@ -279,6 +269,51 @@ private boolean isWidgetInControlHierarchy(Object widget, Control control) { return false; } + /** + * Returns true if the widget is a Text, Combo, CCombo, or StyledText. StyledText is resolved via + * reflection so it is not referenced when it is not on the classpath (e.g. in RAP/Hop Web). + */ + private static boolean isTextLikeWidget(Widget widget) { + if (widget == null) { + return false; + } + return widget instanceof Text + || widget instanceof Combo + || widget instanceof CCombo + || isStyledText(widget); + } + + /** + * Keys that text-like widgets must handle themselves: copy/cut/paste/select-all, + * delete/backspace, and caret / selection navigation (arrows, home/end, page up/down) without + * CTRL/CMD/ALT. Shift alone is allowed so Shift+Arrow selection stays in the widget. + */ + private static boolean isNativeTextEditingKey(KeyEvent event) { + if ((event.stateMask & (SWT.CONTROL | SWT.COMMAND)) != 0) { + char key = Character.toLowerCase((char) event.keyCode); + if (key == 'a' || key == 'c' || key == 'v' || key == 'x') { + return true; + } + } + if (event.keyCode == SWT.DEL || event.character == SWT.BS) { + return true; + } + // Caret movement / selection: leave for the widget unless an app modifier is held + boolean hasAppModifier = (event.stateMask & (SWT.CONTROL | SWT.COMMAND | SWT.ALT)) != 0; + if (!hasAppModifier) { + int keyCode = event.keyCode & SWT.KEY_MASK; + return keyCode == SWT.ARROW_LEFT + || keyCode == SWT.ARROW_RIGHT + || keyCode == SWT.ARROW_UP + || keyCode == SWT.ARROW_DOWN + || keyCode == SWT.HOME + || keyCode == SWT.END + || keyCode == SWT.PAGE_UP + || keyCode == SWT.PAGE_DOWN; + } + return false; + } + /** * Returns true if the widget is a StyledText. Uses reflection so that StyledText is not * referenced when it is not on the classpath (e.g. in RAP/Hop Web).