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 029c173ae1 small multi-line improvements, fixes #7462 (#7591)
029c173ae1 is described below
commit 029c173ae1c6338d996afc29816fa1726b441eb0
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Jul 22 15:59:12 2026 +0200
small multi-line improvements, fixes #7462 (#7591)
* small multi-line improvements, fixes #7462
* fix hop web
---
.../core/widget/TableViewMultilineEditorTest.java | 582 +++++++++++++++++++++
.../org/apache/hop/ui/core/widget/TableView.java | 221 ++++++--
.../core/widget/messages/messages_en_US.properties | 4 +-
3 files changed, 750 insertions(+), 57 deletions(-)
diff --git
a/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewMultilineEditorTest.java
b/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewMultilineEditorTest.java
new file mode 100644
index 0000000000..2a9bd35a34
--- /dev/null
+++
b/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewMultilineEditorTest.java
@@ -0,0 +1,582 @@
+/*
+ * 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.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Covers how a table cell is expanded into the floating multi-line editor:
the "expand" icon on the
+ * inline editor, the pop-out it opens, and committing / cancelling from there.
+ *
+ * <p>These paths break silently when touched. The icon has to stay a
non-focusable {@link Label} -
+ * a {@link org.eclipse.swt.widgets.Button} takes focus on mouse-down, which
fires the editor's
+ * focus-lost handler and disposes the editor before the click is ever
handled. And the inline
+ * editor is wrapped in a holder composite that has to come down together with
it.
+ *
+ * <p>Clicks are delivered as synthetic events, which does not reproduce the
platform's focus
+ * transfer, so no test here can observe a focus-stealing icon directly. {@link
+ * #expandingCarriesOverTextTypedInTheInlineEditor()} covers the consequence
instead: an editor that
+ * was committed and disposed before the click is handled cannot hand its
in-flight text over.
+ */
+@Tag("uitest")
+class TableViewMultilineEditorTest extends SwtBotTestBase {
+
+ private static final String SHORT_VALUE = "a short single line value";
+ private static final String MULTILINE_VALUE = "SELECT *\nFROM
customers\nWHERE id > 100";
+
+ private static final String EXPAND_TOOLTIP =
+ BaseMessages.getString(TableView.class, "TableView.tooltip.ExpandValue");
+
+ /** Row holding {@link #MULTILINE_VALUE}, row holding {@link #SHORT_VALUE},
value column. */
+ private static final int MULTILINE_ROW = 0;
+
+ private static final int SHORT_ROW = 1;
+ private static final int VALUE_COLUMN = 2;
+
+ @Test
+ void textCellEditorOffersAnExpandIcon() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ assertNotNull(
+ findExpandIcon(tableView), "the inline editor of a text cell
should carry the icon");
+ });
+ }
+
+ @Test
+ void passwordCellEditorHasNoExpandIcon() {
+ withTableView(
+ true,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ assertNull(
+ findExpandIcon(tableView), "a password cell must not offer a
multi-line pop-out");
+ });
+ }
+
+ @Test
+ void clickingTheExpandIconOpensThePopOutWithTheCellValue() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ clickExpandIcon(tableView);
+
+ Text popOut = awaitPopOutText(bot);
+ assertNotNull(popOut, "clicking the expand icon should open the
multi-line editor");
+ assertEquals(
+ SHORT_VALUE, onUi(popOut::getText), "the pop-out should show the
full cell value");
+ });
+ }
+
+ /**
+ * Expanding mid-edit has to carry the editor's current text over, not fall
back to the value
+ * stored in the cell - otherwise whatever was typed before reaching for the
icon is lost.
+ */
+ @Test
+ void expandingCarriesOverTextTypedInTheInlineEditor() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ onUi(
+ () -> {
+ setInlineEditorText(tableView, "typed but not committed");
+ return null;
+ });
+
+ clickExpandIcon(tableView);
+
+ Text popOut = awaitPopOutText(bot);
+ assertNotNull(popOut, "the pop-out should be open");
+ assertEquals(
+ "typed but not committed",
+ onUi(popOut::getText),
+ "the pop-out should continue the in-flight edit, not re-read the
cell");
+ });
+ }
+
+ @Test
+ void enterInThePopOutCommitsTheEditedValueBackToTheCell() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ clickExpandIcon(tableView);
+ Text popOut = awaitPopOutText(bot);
+ assertNotNull(popOut, "the pop-out should be open");
+
+ onUi(
+ () -> {
+ popOut.setText("first line" + Text.DELIMITER + "second line");
+ return pressEnter(popOut, SWT.NONE);
+ });
+
+ // Line breaks are stored platform-independently, whatever delimiter
the Text used.
+ assertEquals("first line\nsecond line", cellValue(tableView,
SHORT_ROW, VALUE_COLUMN));
+ assertNull(onUi(TableViewMultilineEditorTest::findPopOutTextOnUi),
"Enter should close");
+ });
+ }
+
+ /** Enter has to be swallowed, or the newline lands in the value that is
about to be stored. */
+ @Test
+ void enterDoesNotLeakItsNewlineIntoTheStoredValue() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ clickExpandIcon(tableView);
+ Text popOut = awaitPopOutText(bot);
+ assertNotNull(popOut, "the pop-out should be open");
+
+ Boolean doit = onUi(() -> pressEnter(popOut, SWT.NONE));
+
+ assertEquals(Boolean.FALSE, doit, "a committing Enter must not be
typed into the text");
+ });
+ }
+
+ @Test
+ void shiftEnterInThePopOutAddsALineInsteadOfCommitting() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ clickExpandIcon(tableView);
+ Text popOut = awaitPopOutText(bot);
+ assertNotNull(popOut, "the pop-out should be open");
+
+ Boolean doit = onUi(() -> pressEnter(popOut, SWT.SHIFT));
+
+ assertEquals(Boolean.TRUE, doit, "Shift+Enter must reach the Text to
insert a line");
+ assertNotNull(
+ onUi(TableViewMultilineEditorTest::findPopOutTextOnUi),
+ "Shift+Enter should leave the editor open");
+ assertEquals(
+ SHORT_VALUE,
+ cellValue(tableView, SHORT_ROW, VALUE_COLUMN),
+ "Shift+Enter should not commit");
+ });
+ }
+
+ /**
+ * Shift+Enter means "give me a line break" in both editors. The inline one
cannot, so it hands
+ * over to the pop-out - which is also where Shift+Enter genuinely inserts
the line.
+ */
+ @Test
+ void shiftEnterInTheInlineEditorOpensThePopOut() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ onUi(
+ () -> {
+ setInlineEditorText(tableView, "half typed");
+ return null;
+ });
+
+ Boolean doit = onUi(() -> pressEnter(findTextIn(tableView.table),
SWT.SHIFT));
+
+ assertEquals(
+ Boolean.FALSE,
+ doit,
+ "Shift+Enter must not fall through to the commit-and-close
path");
+ Text popOut = awaitPopOutText(bot);
+ assertNotNull(popOut, "Shift+Enter should open the multi-line
editor");
+ assertEquals(
+ "half typed",
+ onUi(popOut::getText),
+ "the pop-out should continue the in-flight edit, not re-read the
cell");
+ });
+ }
+
+ @Test
+ void plainEnterInTheInlineEditorStillCommitsWithoutExpanding() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ onUi(
+ () -> {
+ setInlineEditorText(tableView, "committed inline");
+ return null;
+ });
+
+ onUi(() -> pressEnter(findTextIn(tableView.table), SWT.NONE));
+
+ assertNull(awaitNoPopOut(bot), "a plain Enter should not expand the
cell");
+ assertEquals("committed inline", cellValue(tableView, SHORT_ROW,
VALUE_COLUMN));
+ });
+ }
+
+ @Test
+ void shiftEnterInAPasswordCellDoesNotOpenThePopOut() {
+ withTableView(
+ true,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+
+ onUi(() -> pressEnter(findTextIn(tableView.table), SWT.SHIFT));
+
+ assertNull(
+ awaitNoPopOut(bot), "a password value must never be shown in the
multi-line editor");
+ });
+ }
+
+ @Test
+ void escapeInThePopOutLeavesTheCellUntouched() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ clickExpandIcon(tableView);
+ Text popOut = awaitPopOutText(bot);
+ assertNotNull(popOut, "the pop-out should be open");
+
+ onUi(
+ () -> {
+ popOut.setText("discarded");
+ Event escape = new Event();
+ escape.keyCode = SWT.ESC;
+ popOut.notifyListeners(SWT.KeyDown, escape);
+ return null;
+ });
+
+ assertEquals(SHORT_VALUE, cellValue(tableView, SHORT_ROW,
VALUE_COLUMN));
+ });
+ }
+
+ @Test
+ void aValueThatAlreadyHasLineBreaksOpensThePopOutDirectly() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ // A single-line inline editor cannot represent this value, so
editing must skip it.
+ editCell(tableView, MULTILINE_ROW, VALUE_COLUMN);
+
+ Text popOut = awaitPopOutText(bot);
+ assertNotNull(popOut, "a value with line breaks should open the
pop-out straight away");
+ assertEquals(MULTILINE_VALUE,
toUnixLineBreaks(onUi(popOut::getText)));
+ assertNull(
+ findExpandIcon(tableView),
+ "the inline editor should be skipped, not opened underneath the
pop-out");
+ });
+ }
+
+ /**
+ * Double-click belongs to the text editor, where it selects a word. It used
to open the pop-out -
+ * both from the table and from the inline editor - which cost users
word-select in every grid.
+ */
+ @Test
+ void doubleClickingACellDoesNotOpenThePopOut() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ // Establish an active cell first, then close the editor again: the
handler this guards
+ // against keyed off the active cell, so without one the test would
pass vacuously.
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ onUi(
+ () -> {
+ tableView.unEdit();
+ tableView.table.notifyListeners(SWT.MouseDoubleClick, new
Event());
+ return null;
+ });
+
+ assertNull(
+ awaitNoPopOut(bot),
+ "double-clicking a cell should no longer open the multi-line
box");
+ });
+ }
+
+ @Test
+ void doubleClickingInsideTheInlineEditorDoesNotOpenThePopOut() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+
+ onUi(
+ () -> {
+ Text inline = findTextIn(tableView.table);
+ assertNotNull(inline, "precondition: the inline editor is
open");
+ inline.notifyListeners(SWT.MouseDown, new Event());
+ inline.notifyListeners(SWT.MouseDoubleClick, new Event());
+ return null;
+ });
+
+ assertNull(
+ awaitNoPopOut(bot),
+ "double-clicking inside the inline editor should select a word,
not expand");
+ });
+ }
+
+ /**
+ * The inline editor of a text cell lives in a holder composite. However the
edit ends, that
+ * holder has to go - a leftover would sit on top of the table and swallow
clicks - but it must
+ * survive the turn in which the editor closes.
+ *
+ * <p>That delay is what makes the expand icon work in Hop Web. Clicking it
blurs the editor, and
+ * RWT hands the server the resulting focus-lost BEFORE the icon's own
mouse-down. Disposing the
+ * holder synchronously there takes the icon with it, and RWT then drops the
pending click, so the
+ * pop-out never opens.
+ */
+ @Test
+ void theHolderOutlivesTheEditorByOneUiTurnAndIsThenDisposed() {
+ withTableView(
+ false,
+ (tableView, bot) -> {
+ editCell(tableView, SHORT_ROW, VALUE_COLUMN);
+ assertNotNull(findExpandIcon(tableView), "precondition: the inline
editor is open");
+
+ // Both in one syncExec: nothing may run in between, so this really
is "right after".
+ int rightAfterClosing =
+ onUi(
+ () -> {
+ tableView.unEdit();
+ return countCompositesOnTable(tableView);
+ });
+ assertEquals(
+ 1,
+ rightAfterClosing,
+ "the holder must still be alive so a pending expand click can
still be delivered");
+
+ bot.sleep(500); // let the deferred disposal run
+
+ assertEquals(
+ 0,
+ (int) onUi(() -> countCompositesOnTable(tableView)),
+ "the holder should be gone once the event loop has turned");
+ });
+ }
+
+ // --- scene
-----------------------------------------------------------------------------------
+
+ /**
+ * Builds a table holding a multi-line row and a short single-line row, then
runs {@code body}.
+ */
+ private void withTableView(boolean passwordColumn, BiConsumer<TableView,
SWTBot> body) {
+ AtomicReference<TableView> tableViewRef = new AtomicReference<>();
+ withScene(
+ shell -> {
+ shell.setLayout(new FillLayout());
+ shell.setSize(900, 320);
+ ColumnInfo[] columns = {
+ new ColumnInfo("Name", ColumnInfo.COLUMN_TYPE_TEXT, false, false),
+ new ColumnInfo("Value", ColumnInfo.COLUMN_TYPE_TEXT, false, false),
+ };
+ columns[1].setPasswordField(passwordColumn);
+ TableView tableView =
+ new TableView(
+ new Variables(),
+ shell,
+ SWT.BORDER | SWT.FULL_SELECTION,
+ columns,
+ 3,
+ null,
+ PropsUi.getInstance());
+ TableItem multiline = tableView.table.getItem(MULTILINE_ROW);
+ multiline.setText(1, "query");
+ multiline.setText(VALUE_COLUMN, MULTILINE_VALUE);
+ TableItem single = tableView.table.getItem(SHORT_ROW);
+ single.setText(1, "note");
+ single.setText(VALUE_COLUMN, SHORT_VALUE);
+ tableView.optWidth(true);
+ tableViewRef.set(tableView);
+ },
+ bot -> body.accept(tableViewRef.get(), bot));
+ }
+
+ // --- interactions
----------------------------------------------------------------------------
+
+ private void editCell(TableView tableView, int rowNr, int colNr) {
+ onUi(
+ () -> {
+ tableView.edit(rowNr, colNr);
+ return null;
+ });
+ }
+
+ /**
+ * Sends Enter to a text widget and reports whether the key was left for it
to consume - i.e.
+ * whether it inserted a line break rather than being taken over by a
handler.
+ */
+ private static Boolean pressEnter(Text text, int stateMask) {
+ Event enter = new Event();
+ enter.keyCode = SWT.CR;
+ enter.character = SWT.CR;
+ enter.stateMask = stateMask;
+ enter.doit = true;
+ text.notifyListeners(SWT.KeyDown, enter);
+ return enter.doit;
+ }
+
+ /** Fires the icon's mouse-down the way a real click would. */
+ private void clickExpandIcon(TableView tableView) {
+ onUi(
+ () -> {
+ Label icon = findExpandIconOnUi(tableView);
+ assertNotNull(icon, "no expand icon on the inline editor");
+ icon.notifyListeners(SWT.MouseDown, new Event());
+ return null;
+ });
+ }
+
+ // --- lookups
---------------------------------------------------------------------------------
+
+ private Label findExpandIcon(TableView tableView) {
+ return onUi(() -> findExpandIconOnUi(tableView));
+ }
+
+ private static Label findExpandIconOnUi(TableView tableView) {
+ return findLabelWithExpandTooltip(tableView.table);
+ }
+
+ private static Label findLabelWithExpandTooltip(Composite parent) {
+ for (Control child : parent.getChildren()) {
+ // Match on the tooltip: a TextVar carries an image label of its own
(the $ variable hint).
+ if (child instanceof Label label &&
EXPAND_TOOLTIP.equals(label.getToolTipText())) {
+ return label;
+ }
+ if (child instanceof Composite composite) {
+ Label found = findLabelWithExpandTooltip(composite);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ /** Types into the open inline editor, i.e. the Text somewhere inside the
holder on the table. */
+ private static void setInlineEditorText(TableView tableView, String value) {
+ Text editor = findTextIn(tableView.table);
+ assertNotNull(editor, "no inline editor is open");
+ editor.setText(value);
+ }
+
+ private static Text findTextIn(Composite parent) {
+ for (Control child : parent.getChildren()) {
+ if (child instanceof Text text) {
+ return text;
+ }
+ if (child instanceof Composite composite) {
+ Text found = findTextIn(composite);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ /** Composites parented straight onto the table - i.e. surviving inline
editor holders. */
+ private static int countCompositesOnTable(TableView tableView) {
+ int count = 0;
+ for (Control child : tableView.table.getChildren()) {
+ if (child instanceof Composite && !child.isDisposed()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private String cellValue(TableView tableView, int rowNr, int colNr) {
+ return onUi(() -> tableView.table.getItem(rowNr).getText(colNr));
+ }
+
+ /**
+ * The pop-out is a title-less shell of its own, so SWTBot cannot address it
by name - find its
+ * multi-line Text directly. Polls, because the icon handler opens the
pop-out asynchronously.
+ */
+ private Text awaitPopOutText(SWTBot bot) {
+ for (int attempt = 0; attempt < 40; attempt++) {
+ Text found = onUi(TableViewMultilineEditorTest::findPopOutTextOnUi);
+ if (found != null) {
+ return found;
+ }
+ bot.sleep(50);
+ }
+ return null;
+ }
+
+ /**
+ * Waits out the grace period a deferred handler would need to open a
pop-out, then reports what
+ * is actually there - for the cases where the answer should be "nothing".
+ */
+ private Text awaitNoPopOut(SWTBot bot) {
+ bot.sleep(500);
+ return onUi(TableViewMultilineEditorTest::findPopOutTextOnUi);
+ }
+
+ private static Text findPopOutTextOnUi() {
+ for (Shell shell : display.getShells()) {
+ for (Control child : shell.getChildren()) {
+ if (child instanceof Text text && (text.getStyle() & SWT.MULTI) != 0) {
+ return text;
+ }
+ }
+ }
+ return null;
+ }
+
+ // --- helpers
---------------------------------------------------------------------------------
+
+ /** Runs {@code supplier} on the UI thread and hands its result back to the
SWTBot worker. */
+ private static <T> T onUi(Supplier<T> supplier) {
+ AtomicReference<T> result = new AtomicReference<>();
+ AtomicReference<RuntimeException> failure = new AtomicReference<>();
+ display.syncExec(
+ () -> {
+ try {
+ result.set(supplier.get());
+ } catch (RuntimeException e) {
+ failure.set(e);
+ }
+ });
+ if (failure.get() != null) {
+ throw failure.get();
+ }
+ return result.get();
+ }
+
+ private static String toUnixLineBreaks(String value) {
+ return value == null ? null : value.replace("\r\n", "\n").replace('\r',
'\n');
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
index 9d64814769..e478014edf 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
@@ -188,8 +188,11 @@ public class TableView extends Composite {
@Getter private int activeTableColumn;
private int activeTableRow;
- /** When the inline editor was last opened (ms), used to catch a
double-click's second click. */
- private long inlineEditorOpenedAt;
+ /**
+ * The wrapper the inline editor lives in when the cell carries the "expand"
icon, or null when
+ * the editor sits in the cell directly. Disposed together with {@link
#text}.
+ */
+ private Composite inlineEditorHolder;
/** The currently open multi-line pop-out editor, if any (guards against
stacking two). */
private Shell multilineShell;
@@ -626,8 +629,9 @@ public class TableView extends Composite {
// Table listens to the mouse:
table.addMouseListener(createTableMouseListener());
- // Double-click a text cell to edit its value in a floating multi-line
editor.
- table.addListener(SWT.MouseDoubleClick, e -> onCellDoubleClick());
+ // Double-click is deliberately NOT bound here: it belongs to the text
editor, where it selects
+ // a word. The multi-line editor is reached through the expand icon on the
inline editor, and
+ // opens on its own for values that already contain line breaks.
// Desktop only: draw long / multi-line text cells shortened &
single-lined, while getText()
// keeps returning the full value. RWT does not deliver the custom
item-draw events, so in
@@ -1134,6 +1138,15 @@ public class TableView extends Composite {
boolean right = false;
boolean left = false;
+ // Shift+Enter asks for a line break, which a single-line editor
cannot give. Hand the value
+ // to the multi-line pop-out instead - the same key that adds a line
once you are in there.
+ if ((e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR || e.character
== SWT.CR)
+ && (e.stateMask & SWT.SHIFT) != 0) {
+ e.doit = false;
+ expandActiveCell();
+ return;
+ }
+
// "ENTER": close the text editor and copy the data over
// We edit the data after moving to another cell, only if editNextCell
=
if (e.character == SWT.CR
@@ -1198,7 +1211,7 @@ public class TableView extends Composite {
}
} else if (e.keyCode == SWT.ESC) {
- text.dispose();
+ disposeInlineEditor();
table.setFocus();
}
}
@@ -1279,6 +1292,7 @@ public class TableView extends Composite {
final int colNr = activeTableColumn;
final int rowNr = table.indexOf(row);
final Control ftext = text;
+ final Composite fholder = inlineEditorHolder;
final String[] fBeforeEdit = beforeEdit;
@@ -1295,7 +1309,7 @@ public class TableView extends Composite {
return;
}
row.setText(colNr, value);
- ftext.dispose();
+ disposeInlineEditor(ftext, fholder);
String[] afterEdit = getItemText(row);
checkChanged(
@@ -1853,6 +1867,42 @@ public class TableView extends Composite {
}
}
+ /**
+ * Tear down the inline editor. Text cells wrap their editor in a holder
composite so it can carry
+ * the "expand to multi-line editor" icon; disposing the editor alone would
leave that wrapper
+ * hanging over the cell, so always route removal through here.
+ */
+ private void disposeInlineEditor() {
+ disposeInlineEditor(text, inlineEditorHolder);
+ }
+
+ /** Variant for call sites that captured the editor and its holder before
going asynchronous. */
+ private void disposeInlineEditor(Control editorControl, Composite holder) {
+ if (editorControl != null && !editorControl.isDisposed()) {
+ editorControl.dispose();
+ }
+ if (holder == null) {
+ return;
+ }
+ if (inlineEditorHolder == holder) {
+ inlineEditorHolder = null;
+ }
+ // The holder outlives the editor by one event-loop turn on purpose.
Clicking the expand icon
+ // blurs the editor, and in Hop Web the resulting focus-lost is processed
BEFORE the icon's own
+ // mouse-down: disposing the holder right here would take the icon down
with it and RWT then
+ // drops the pending click, so the pop-out never opens. Deferring lets the
click be delivered
+ // first. The holder is empty by then and goes away inside the same
request / UI turn.
+ if (!holder.isDisposed()) {
+ holder.getDisplay().asyncExec(() -> disposeHolder(holder));
+ }
+ }
+
+ private static void disposeHolder(Composite holder) {
+ if (!holder.isDisposed()) {
+ holder.dispose();
+ }
+ }
+
private void applyTextChange(TableItem row, int rowNr, int colNr) {
if (text == null || text.isDisposed()) {
return;
@@ -1860,7 +1910,7 @@ public class TableView extends Composite {
String textData = getTextWidgetValue(colNr);
row.setText(colNr, textData);
- text.dispose();
+ disposeInlineEditor();
table.setFocus();
tableViewModifyListener.cellFocusLost(rowNr);
@@ -1952,12 +2002,11 @@ public class TableView extends Composite {
}
/**
- * Double-clicking an editable text cell opens a lightweight, non-modal
multi-line editor anchored
- * to the cell (the same floating-shell idea as the Ctrl+Space variable
helper). Handy for long
- * values, SQL, JSON and multi-line content. Combo/button/read-only cells
keep their own
- * behaviour.
+ * Open the multi-line editor on the cell currently being edited, carrying
over whatever stands in
+ * the inline editor. Silently does nothing for cells that have no pop-out
(combo, button,
+ * password, read-only or disabled), so it is safe to wire to a key stroke
that is not cell-aware.
*/
- private void onCellDoubleClick() {
+ private void expandActiveCell() {
if (readonly || !table.isEnabled() || !isEnabled() || columns.length == 0)
{
return;
}
@@ -1973,7 +2022,8 @@ public class TableView extends Composite {
if (colinfo == null
|| (colinfo.getType() != ColumnInfo.COLUMN_TYPE_TEXT
&& colinfo.getType() != ColumnInfo.COLUMN_TYPE_TEXT_BUTTON)
- || colinfo.isReadOnly()) {
+ || colinfo.isReadOnly()
+ || colinfo.isPasswordField()) {
return;
}
if (colinfo.getDisabledListener() != null
@@ -1983,9 +2033,14 @@ public class TableView extends Composite {
editMultiline(activeTableItem, rowNr, colNr, colinfo);
}
+ /**
+ * Opens a lightweight, non-modal multi-line editor anchored to the cell
(the same floating-shell
+ * idea as the Ctrl+Space variable helper). Handy for long values, SQL, JSON
and multi-line
+ * content. Reached from the expand icon on the inline editor or with
Shift+Enter, and opened
+ * automatically for values that already contain line breaks.
+ */
private void editMultiline(TableItem row, int rowNr, int colNr, ColumnInfo
colinfo) {
- // A pop-out is already open (e.g. both a double-click and its
second-click fallback fired) —
- // don't stack a second one.
+ // A pop-out is already open — don't stack a second one.
if (multilineShell != null && !multilineShell.isDisposed()) {
multilineShell.setFocus();
return;
@@ -1995,7 +2050,7 @@ public class TableView extends Composite {
String seed;
if (text != null && !text.isDisposed()) {
seed = getTextWidgetValue(colNr);
- text.dispose();
+ disposeInlineEditor();
} else {
seed = row.getText(colNr);
}
@@ -2025,6 +2080,8 @@ public class TableView extends Composite {
final Text multi = new Text(popup, SWT.MULTI | SWT.WRAP | SWT.V_SCROLL |
SWT.BORDER);
PropsUi.setLook(multi);
+ // Enter closing the editor is surprising in a multi-line box, so spell
the keys out.
+ multi.setToolTipText(BaseMessages.getString(PKG,
"TableView.tooltip.MultilineEditorKeys"));
// Seed with the platform delimiter so line breaks render (Windows needs
\r\n).
multi.setText(toPlatformLineBreaks(seed));
@@ -2073,16 +2130,20 @@ public class TableView extends Composite {
}
};
- // Ctrl+Enter commits; Escape cancels. (Clicking away commits too — armed
below via shell
- // deactivation rather than a text focus-out, so grabbing a resize edge
doesn't
- // commit-and-close.)
+ // Enter commits and closes; only Shift+Enter inserts a line break. Escape
cancels. (Clicking
+ // away commits too — armed below via shell deactivation rather than a
text focus-out, so
+ // grabbing a resize edge doesn't commit-and-close.)
multi.addListener(
SWT.KeyDown,
e -> {
if (e.keyCode == SWT.ESC) {
cancel.run();
- } else if ((e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR)
- && (e.stateMask & SWT.MOD1) != 0) {
+ } else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
+ if ((e.stateMask & SWT.SHIFT) != 0) {
+ return; // let the Text insert the line break
+ }
+ // Swallow the key so the newline is not typed into the value we
are about to store.
+ e.doit = false;
commit.run();
}
});
@@ -2905,7 +2966,7 @@ public class TableView extends Composite {
}
if (text != null && !text.isDisposed()) {
- text.dispose();
+ disposeInlineEditor();
}
if (colinfo.getSelectionAdapter() != null) {
@@ -2934,6 +2995,27 @@ public class TableView extends Composite {
final ModifyListener modifyListener = me ->
setColumnWidthBasedOnTextField(colNr, useVariables);
+ // Text cells get an "expand" icon on the right edge of the inline editor
which opens the
+ // multi-line pop-out. Holding both needs a wrapper composite around the
editor, so only create
+ // one where the pop-out actually applies. Passwords never expand.
+ final boolean expandable =
+ !passwordField
+ && (colinfo.getType() == ColumnInfo.COLUMN_TYPE_TEXT
+ || colinfo.getType() == ColumnInfo.COLUMN_TYPE_TEXT_BUTTON);
+ final Composite editorParent;
+ if (expandable) {
+ inlineEditorHolder = new Composite(table, SWT.NONE);
+ PropsUi.setLook(inlineEditorHolder);
+ FormLayout holderLayout = new FormLayout();
+ holderLayout.marginWidth = 0;
+ holderLayout.marginHeight = 0;
+ inlineEditorHolder.setLayout(holderLayout);
+ editorParent = inlineEditorHolder;
+ } else {
+ inlineEditorHolder = null;
+ editorParent = table;
+ }
+
if (useVariables) {
IGetCaretPosition getCaretPositionInterface =
() -> ((TextVar) text).getTextWidget().getCaretPosition();
@@ -2959,19 +3041,20 @@ public class TableView extends Composite {
if (passwordField) {
textWidget =
new PasswordTextVar(
- variables, table, SWT.NONE, getCaretPositionInterface,
insertTextInterface);
+ variables, editorParent, SWT.NONE, getCaretPositionInterface,
insertTextInterface);
} else if (isTextButton) {
textWidget =
new TextVarButton(
variables,
- table,
+ editorParent,
SWT.NONE,
getCaretPositionInterface,
insertTextInterface,
columnInfo.getTextVarButtonSelectionListener());
} else {
textWidget =
- new TextVar(variables, table, SWT.NONE, getCaretPositionInterface,
insertTextInterface);
+ new TextVar(
+ variables, editorParent, SWT.NONE, getCaretPositionInterface,
insertTextInterface);
}
text = textWidget;
@@ -2995,7 +3078,7 @@ public class TableView extends Composite {
textWidget.addListener(SWT.KeyUp, lsKeyUp);
}
} else {
- Text textWidget = new Text(table, SWT.NONE);
+ Text textWidget = new Text(editorParent, SWT.NONE);
text = textWidget;
textWidget.setText(content);
if (lsMod != null) {
@@ -3019,34 +3102,11 @@ public class TableView extends Composite {
}
PropsUi.setLook(text);
- // Double-clicking inside the inline editor expands it into the multi-line
editor. The inline
- // editor is created on the first click, so on some platforms (Windows)
the second click of a
- // double-click lands on this fresh editor as a single click and neither
the table nor the
- // editor sees a full double-click. So also treat a click arriving within
the OS double-click
- // time of the editor opening as that second click. Deferred so this
handler finishes before the
- // inline editor it belongs to is disposed.
- if (!passwordField) {
- Control innerText = (text instanceof TextVar tv) ? tv.getTextWidget() :
text;
- Runnable expand =
- () ->
- getDisplay()
- .asyncExec(
- () -> {
- if (!isDisposed()) {
- editMultiline(row, rowNr, colNr, colinfo);
- }
- });
- innerText.addListener(SWT.MouseDoubleClick, e -> expand.run());
- innerText.addListener(
- SWT.MouseDown,
- e -> {
- if (System.currentTimeMillis() - inlineEditorOpenedAt
- <= getDisplay().getDoubleClickTime()) {
- expand.run();
- }
- });
+ Control editorControl = text;
+ if (expandable) {
+ editorControl = inlineEditorHolder;
+ addExpandIcon(inlineEditorHolder, row, rowNr, colNr, colinfo);
}
- inlineEditorOpenedAt = System.currentTimeMillis();
int width = tableColumn[colNr].getWidth();
int height = 30;
@@ -3055,13 +3115,62 @@ public class TableView extends Composite {
editor.grabHorizontal = true;
// Open the text editor in the correct column of the selected row.
- editor.setEditor(text, row, colNr);
+ editor.setEditor(editorControl, row, colNr);
text.setFocus();
- text.setSize(width, height);
+ editorControl.setSize(width, height);
editor.layout();
}
+ /**
+ * Put a small "expand" icon on the right edge of the inline editor which
opens the value in the
+ * floating multi-line editor. It is a {@link Label} rather than a {@link
Button} on purpose: a
+ * label does not take focus, so clicking it does not fire the editor's
focus-lost handler (which
+ * would commit and dispose the editor before the click was ever handled).
+ */
+ private void addExpandIcon(
+ Composite holder, TableItem row, int rowNr, int colNr, ColumnInfo
colinfo) {
+ Label expandLabel = new Label(holder, SWT.NONE);
+ PropsUi.setLook(expandLabel);
+ expandLabel.setImage(GuiResource.getInstance().getImageMaximizePanel());
+ expandLabel.setToolTipText(BaseMessages.getString(PKG,
"TableView.tooltip.ExpandValue"));
+
+ // The holder sits on top of the table cell, so without a background of
its own the cell's own
+ // text shows through around the icon. Take the editor's background so the
two read as one field
+ // in both the light and the dark theme.
+ Color editorBackground =
+ (text instanceof TextVar textVar)
+ ? textVar.getTextWidget().getBackground()
+ : text.getBackground();
+ holder.setBackground(editorBackground);
+ expandLabel.setBackground(editorBackground);
+
+ FormData fdExpand = new FormData();
+ fdExpand.top = new FormAttachment(0, 0);
+ fdExpand.right = new FormAttachment(100, -2);
+ fdExpand.bottom = new FormAttachment(100, 0);
+ expandLabel.setLayoutData(fdExpand);
+
+ FormData fdText = new FormData();
+ fdText.left = new FormAttachment(0, 0);
+ fdText.top = new FormAttachment(0, 0);
+ fdText.right = new FormAttachment(expandLabel, -2);
+ fdText.bottom = new FormAttachment(100, 0);
+ text.setLayoutData(fdText);
+
+ // Deferred so this handler finishes before the inline editor it belongs
to is disposed.
+ expandLabel.addListener(
+ SWT.MouseDown,
+ e ->
+ getDisplay()
+ .asyncExec(
+ () -> {
+ if (!isDisposed()) {
+ editMultiline(row, rowNr, colNr, colinfo);
+ }
+ }));
+ }
+
private void setColumnWidthBasedOnTextField(final int colNr, final boolean
useVariables) {
if (!columns[colNr - 1].isAutoResize()) {
return;
@@ -3864,7 +3973,7 @@ public class TableView extends Composite {
public void unEdit() {
if (text != null && !text.isDisposed()) {
- text.dispose();
+ disposeInlineEditor();
text = null;
}
if (comboVar != null && !comboVar.isDisposed()) {
diff --git
a/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
index d8ac9eb19b..5757471c35 100644
---
a/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
@@ -84,10 +84,12 @@ TableView.ToolBarWidget.DeleteSelected.ToolTip=Delete the
selected rows
TableView.ToolBarWidget.InsertRowAfter.ToolTip=Add an empty row after the
selected line
TableView.ToolBarWidget.InsertRowBefore.ToolTip=Add an empty row before the
selected line
TableView.ToolBarWidget.KeepSelected.ToolTip=Keep the selected rows, delete
the rest
-TableView.WebNewlineHint.Label=* Line breaks aren't shown in table cells in
the web version (known limitation). Double-click a cell to view or edit the
full text.
+TableView.WebNewlineHint.Label=* Line breaks aren't shown in table cells in
the web version (known limitation). Click the expand icon in a cell to view or
edit the full text.
TableView.ToolBarWidget.MoveRowsDown.ToolTip=Move selected rows down
TableView.ToolBarWidget.MoveRowsUp.ToolTip=Move selected rows up
TableView.ToolBarWidget.NavigateToColumn.ToolTip=Navigate to a column
+TableView.tooltip.ExpandValue=Edit this value in a multi-line editor
(Shift+Enter)
+TableView.tooltip.MultilineEditorKeys=Enter\: save and close - Shift+Enter\:
new line - Esc\: cancel
TableView.ToolBarWidget.PasteSelected.ToolTip=Paste clipboard to the table
after the selected row
TableView.ToolBarWidget.RedoAction.ToolTip=Redo the last action
TableView.ToolBarWidget.SelectAll.ToolTip=Select all rows