This is an automated email from the ASF dual-hosted git repository.
bamaer 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 b18fc1f16d Issue #8606 : Select all text in text widgets (#8612)
b18fc1f16d is described below
commit b18fc1f16d94d0355f4d1b19c789291496104212
Author: Matt Casters <[email protected]>
AuthorDate: Fri Sep 25 17:16:46 2026 +0200
Issue #8606 : Select all text in text widgets (#8612)
Ctrl/Cmd+A now selects the contents of a focused text field in Hop and Hop
Web, while the same chord on the canvas still selects the graph.
---
.../main/java/org/apache/hop/ui/hopgui/HopWeb.java | 3 +-
.../org/apache/hop/ui/hopgui/HopWebEntryPoint.java | 2 +
.../org/apache/hop/ui/hopgui/text-select-all.js | 75 ++++++++++++++++++++
.../hop/ui/core/widget/StyledTextSelectAll.java | 38 ++++++++++
.../apache/hop/ui/core/widget/TextSelectAll.java | 82 ++++++++++++++++++++++
.../org/apache/hop/ui/hopgui/HopGuiKeyHandler.java | 36 +++++++---
.../hop/ui/core/widget/TextSelectAllTest.java | 62 ++++++++++++++++
.../apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java | 73 +++++++++++++++++++
8 files changed, 362 insertions(+), 9 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 794da03453..57583f53e4 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
@@ -174,7 +174,8 @@ public class HopWeb implements ApplicationConfiguration {
"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",
- "org/apache/hop/ui/hopgui/text-line-clipboard.js")
+ "org/apache/hop/ui/hopgui/text-line-clipboard.js",
+ "org/apache/hop/ui/hopgui/text-select-all.js")
.forEach(
str ->
application.addResource(
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 ba98a7dfd8..d97b4be258 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
@@ -219,6 +219,8 @@ public class HopWebEntryPoint extends AbstractEntryPoint {
jsLoader.require(macKeysLocation);
// Empty Ctrl/Cmd+C/X copies or cuts the current line. Must run in the key
gesture.
jsLoader.require(resourceManager.getLocation("js/text-line-clipboard.js"));
+ // Ctrl/Cmd+A selects the field. CANCEL_KEYS would otherwise swallow it
(issue #8606).
+ jsLoader.require(resourceManager.getLocation("js/text-select-all.js"));
// Configure keyboard shortcuts for RAP dynamically from annotations
// ACTIVE_KEYS tells RAP to send these key combinations to the server
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/text-select-all.js
b/rap/src/main/resources/org/apache/hop/ui/hopgui/text-select-all.js
new file mode 100644
index 0000000000..4a02c70730
--- /dev/null
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/text-select-all.js
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+/**
+ * Select all text on Ctrl/Cmd+A.
+ *
+ * The same chord selects the graph, so it is in RAP CANCEL_KEYS and the
browser never selects the
+ * field. This runs in the capture phase, before that cancel, and leaves the
event running so RAP
+ * records the new selection. Monaco selects inside its own model and is left
alone.
+ */
+(function () {
+ 'use strict';
+
+ function isTextField(el) {
+ if (!el || !el.tagName) {
+ return false;
+ }
+ var tag = el.tagName.toLowerCase();
+ if (tag === 'textarea') {
+ return true;
+ }
+ if (tag !== 'input') {
+ return false;
+ }
+ var type = (el.type || 'text').toLowerCase();
+ if (type === 'hidden' || type === 'checkbox' || type === 'radio'
+ || type === 'button' || type === 'submit' || type === 'file'
+ || type === 'number' || type === 'range' || type === 'color') {
+ return false;
+ }
+ return typeof el.selectionStart === 'number' && typeof el.selectionEnd ===
'number';
+ }
+
+ function inMonaco(el) {
+ return el.closest && el.closest('.monaco-editor');
+ }
+
+ document.addEventListener('keydown', function (event) {
+ if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey) {
+ return;
+ }
+ var key = (event.key || '').toLowerCase();
+ if (key !== 'a') {
+ return;
+ }
+ var el = event.target;
+ if (!isTextField(el) || inMonaco(el)) {
+ return;
+ }
+ var length = (el.value || '').length;
+ try {
+ el.setSelectionRange(0, length);
+ } catch (e) {
+ try {
+ el.select();
+ } catch (ignored) {
+ // The control does not expose a selection. Leave the key to the
widget.
+ }
+ }
+ }, true);
+})();
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextSelectAll.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextSelectAll.java
new file mode 100644
index 0000000000..ddd4338c21
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextSelectAll.java
@@ -0,0 +1,38 @@
+/*
+ * 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.widget;
+
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.widgets.Widget;
+
+/**
+ * StyledText half of {@link TextSelectAll}. Kept in its own class so Hop Web,
which has no
+ * StyledText, does not load this class.
+ */
+final class StyledTextSelectAll {
+
+ private StyledTextSelectAll() {}
+
+ static boolean selectAll(Widget widget) {
+ if (!(widget instanceof StyledText styledText)) {
+ return false;
+ }
+ styledText.selectAll();
+ return true;
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TextSelectAll.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TextSelectAll.java
new file mode 100644
index 0000000000..0923f8caae
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TextSelectAll.java
@@ -0,0 +1,82 @@
+/*
+ * 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.widget;
+
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Widget;
+
+/**
+ * Selects all of the text in a text widget.
+ *
+ * <p>{@link org.eclipse.swt.custom.StyledText} does not bind Ctrl/Cmd+A, and
on Windows the native
+ * text control does not either. Hop Web cancels that chord so the graph can
use it, which also
+ * takes it away from the browser. Call this from the key handler instead.
+ */
+public final class TextSelectAll {
+
+ private TextSelectAll() {}
+
+ /**
+ * Selects all text in {@code widget}.
+ *
+ * @return {@code true} when the widget supports it. {@code false} for
anything else, including a
+ * widget that was disposed while the key was delivered.
+ */
+ public static boolean selectAll(Widget widget) {
+ if (widget == null) {
+ return false;
+ }
+ try {
+ if (widget instanceof Text text) {
+ text.selectAll();
+ return true;
+ }
+ if (widget instanceof Combo combo) {
+ combo.setSelection(new Point(0, length(combo.getText())));
+ return true;
+ }
+ if (widget instanceof CCombo combo) {
+ combo.setSelection(new Point(0, length(combo.getText())));
+ return true;
+ }
+ if (isStyledText(widget)) {
+ return StyledTextSelectAll.selectAll(widget);
+ }
+ return false;
+ } catch (SWTException e) {
+ return false;
+ }
+ }
+
+ private static int length(String text) {
+ return text == null ? 0 : text.length();
+ }
+
+ private static boolean isStyledText(Widget widget) {
+ try {
+ Class<?> type = Class.forName("org.eclipse.swt.custom.StyledText");
+ return type.isInstance(widget);
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+}
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 172eeca817..7783fbd5ee 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
@@ -32,6 +32,7 @@ import org.apache.hop.core.gui.plugin.key.KeyboardShortcut;
import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.security.ActionPermissionMapper;
import org.apache.hop.ui.core.widget.TextLineClipboard;
+import org.apache.hop.ui.core.widget.TextSelectAll;
import org.apache.hop.ui.hopgui.perspective.IHopPerspective;
import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
@@ -148,8 +149,8 @@ public class HopGuiKeyHandler extends KeyAdapter {
// RAP does not fire focus events for a focus change made in the browser.
//
// The key filter covers every shell on this display, including dialogs
that never register
- // here, so word-movement keys are not stolen and an empty Ctrl/Cmd+C/X
copies or cuts the
- // current line (issue #8362).
+ // here, so word-movement keys are not stolen, Ctrl/Cmd+A selects the
text, and an empty
+ // Ctrl/Cmd+C/X copies or cuts the current line (issues #8362 and #8606).
//
if (display != null && !display.isDisposed() &&
filteredDisplays.add(display)) {
display.addFilter(SWT.FocusIn, event -> attachTo(event.widget));
@@ -511,13 +512,15 @@ public class HopGuiKeyHandler extends KeyAdapter {
* <p>Horizontal Ctrl/Cmd/Alt+Left/Right stay with the widget (word movement
on Windows and Linux,
* Option+Left/Right on macOS, line edges for Command+Left/Right). They must
not align or
* distribute the graph. An empty Ctrl/Cmd+C or Ctrl/Cmd+X copies or cuts
the current line on the
- * desktop; Hop Web does that in the browser, inside the key gesture. Other
text keys (bare
- * arrows, Ctrl+A/V, typing) are not dispatched as shortcuts either. Ctrl+S
and the vertical align
- * shortcuts still run.
+ * desktop; Hop Web does that in the browser, inside the key gesture.
Ctrl/Cmd+A selects all of
+ * the text here: StyledText has no such binding, and Hop Web cancels the
browser's own because
+ * the chord also selects the graph (issue #8606). Other text keys (bare
arrows, Ctrl+V, typing)
+ * are not dispatched as shortcuts either. Ctrl+S and the vertical align
shortcuts still run.
*
* <p>{@code widgets.Event} and {@code events.KeyEvent} are not the same
type, so callers pass the
* fields. {@link TextEditing#stopShortcuts} means do not run a Hop
shortcut. {@link
- * TextEditing#consume} means set {@code doit} false (the line was copied or
cut here).
+ * TextEditing#consume} means set {@code doit} false (the line was copied or
cut, or the text was
+ * selected, here).
*/
private TextEditing applyTextEditingKey(
Widget widget, int keyCode, int stateMask, char character, Display
display) {
@@ -536,8 +539,9 @@ public class HopGuiKeyHandler extends KeyAdapter {
if (isHorizontalWordKey(keyCode, stateMask)) {
return TextEditing.STOP;
}
- if (webEditor && isCopyOrCutKey(keyCode, stateMask)) {
- // Monaco already copied or cut. Do not also copy the graph, and do not
cancel the key.
+ if (webEditor && (isCopyOrCutKey(keyCode, stateMask) ||
isSelectAllKey(keyCode, stateMask))) {
+ // Monaco already copied, cut or selected all. Do not also change the
graph, and do not
+ // cancel the key.
return TextEditing.STOP;
}
if (!textLike || !isNativeTextEditingKey(keyCode, stateMask, character)) {
@@ -550,6 +554,11 @@ public class HopGuiKeyHandler extends KeyAdapter {
&& TextLineClipboard.copyOrCutCurrentLine(widget, isCutKey(keyCode))) {
return TextEditing.CONSUME;
}
+ // text-select-all.js selects immediately in the browser. Selecting here
as well keeps the
+ // server selection in step and covers the desktop, where the widget
itself does not.
+ if (isSelectAllKey(keyCode, stateMask) && TextSelectAll.selectAll(widget))
{
+ return TextEditing.CONSUME;
+ }
return TextEditing.STOP;
}
@@ -597,6 +606,17 @@ public class HopGuiKeyHandler extends KeyAdapter {
return alt || control || command;
}
+ /** Ctrl/Cmd+A with no Alt and no Shift. */
+ private static boolean isSelectAllKey(int keyCode, int stateMask) {
+ if ((stateMask & (SWT.ALT | SWT.SHIFT)) != 0) {
+ return false;
+ }
+ if ((stateMask & (SWT.CONTROL | SWT.COMMAND)) == 0) {
+ return false;
+ }
+ return Character.toLowerCase((char) keyCode) == 'a';
+ }
+
/** Ctrl/Cmd+C or Ctrl/Cmd+X with no Alt and no Shift. */
private static boolean isCopyOrCutKey(int keyCode, int stateMask) {
if ((stateMask & (SWT.ALT | SWT.SHIFT)) != 0) {
diff --git
a/ui/src/test/java/org/apache/hop/ui/core/widget/TextSelectAllTest.java
b/ui/src/test/java/org/apache/hop/ui/core/widget/TextSelectAllTest.java
new file mode 100644
index 0000000000..fae7976766
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/core/widget/TextSelectAllTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.widget;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Text;
+import org.junit.jupiter.api.Test;
+
+class TextSelectAllTest {
+
+ @Test
+ void selectsTextComboAndStyledText() {
+ Text text = mock(Text.class);
+ assertTrue(TextSelectAll.selectAll(text));
+ verify(text).selectAll();
+
+ Combo combo = mock(Combo.class);
+ when(combo.getText()).thenReturn(null);
+ assertTrue(TextSelectAll.selectAll(combo));
+ verify(combo).setSelection(new Point(0, 0));
+
+ CCombo ccombo = mock(CCombo.class);
+ when(ccombo.getText()).thenReturn("name");
+ assertTrue(TextSelectAll.selectAll(ccombo));
+ verify(ccombo).setSelection(new Point(0, 4));
+
+ StyledText styled = mock(StyledText.class);
+ assertTrue(TextSelectAll.selectAll(styled));
+ verify(styled).selectAll();
+ }
+
+ @Test
+ void leavesOtherWidgetsAlone() {
+ assertFalse(TextSelectAll.selectAll(null));
+ assertFalse(TextSelectAll.selectAll(mock(Canvas.class)));
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java
index 74069f36e4..ee5d903b32 100644
--- a/ui/src/test/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java
+++ b/ui/src/test/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -39,8 +40,12 @@ import org.apache.hop.ui.core.widget.TextVar;
import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
@@ -266,6 +271,74 @@ class HopGuiKeyHandlerTest {
}
}
+ /** Stands in for the Edit / Select All shortcut on the graph and the main
menu. */
+ public static class SelectAllGraph {
+ public int selected;
+
+ @GuiKeyboardShortcut(control = true, key = 'a')
+ @GuiOsxKeyboardShortcut(command = true, key = 'a')
+ public void selectAll() {
+ selected++;
+ }
+ }
+
+ @Test
+ void selectAllStaysInTextWidgets() {
+ SelectAllGraph graph = new SelectAllGraph();
+ registerShortcutsLikeHopGuiEnvironment(SelectAllGraph.class);
+
+ HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
+ keyHandler.addParentObjectToHandle(graph);
+ try {
+ Text text = mock(Text.class);
+ KeyEvent ctrl = keyEvent(text, 'a', SWT.CONTROL);
+ keyHandler.keyPressed(ctrl);
+ assertEquals(0, graph.selected, "Ctrl+A in a text field must not select
the graph");
+ assertFalse(ctrl.doit, "Selecting the text consumes the key");
+ verify(text).selectAll();
+
+ KeyEvent command = keyEvent(text, 'A', SWT.COMMAND);
+ keyHandler.keyPressed(command);
+ assertEquals(0, graph.selected, "Command+A selects the field on macOS");
+ assertFalse(command.doit);
+ verify(text, times(2)).selectAll();
+
+ Combo combo = mock(Combo.class);
+ when(combo.getText()).thenReturn("field");
+ KeyEvent comboKey = keyEvent(combo, 'a', SWT.CONTROL);
+ keyHandler.keyPressed(comboKey);
+ assertEquals(0, graph.selected);
+ assertFalse(comboKey.doit);
+ verify(combo).setSelection(new Point(0, 5));
+
+ CCombo ccombo = mock(CCombo.class);
+ when(ccombo.getText()).thenReturn("ab");
+ KeyEvent ccomboKey = keyEvent(ccombo, 'a', SWT.CONTROL);
+ keyHandler.keyPressed(ccomboKey);
+ assertFalse(ccomboKey.doit);
+ verify(ccombo).setSelection(new Point(0, 2));
+
+ StyledText styled = mock(StyledText.class);
+ KeyEvent styledKey = keyEvent(styled, 'a', SWT.CONTROL);
+ keyHandler.keyPressed(styledKey);
+ assertFalse(styledKey.doit, "StyledText has no Ctrl+A binding of its
own");
+ verify(styled).selectAll();
+
+ KeyEvent shifted = keyEvent(text, 'a', SWT.CONTROL | SWT.SHIFT);
+ keyHandler.keyPressed(shifted);
+ assertEquals(0, graph.selected, "Ctrl+Shift+A must not select the
graph");
+ assertTrue(shifted.doit, "Ctrl+Shift+A is not select-all");
+ verify(text, times(2)).selectAll();
+
+ KeyEvent onCanvas = canvasKey('a', SWT.CONTROL);
+ keyHandler.keyPressed(onCanvas);
+ assertEquals(1, graph.selected, "Ctrl+A on the canvas still selects the
graph");
+ assertFalse(onCanvas.doit);
+ } finally {
+ keyHandler.removeParentObjectToHandle(graph);
+ }
+ }
+
@Test
void arrowKeysAreLeftToTablesAndTrees() {
NavigationGraph graph = new NavigationGraph();