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


Reply via email to