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 6c043b4d1d some hardening on preview rows, fixes #7870 (#7881)
6c043b4d1d is described below

commit 6c043b4d1d883fd46280ae2f3ab402e0697ee478
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Aug 11 10:21:23 2026 +0200

    some hardening on preview rows, fixes #7870 (#7881)
---
 ...est.java => SelectRowDialogActivationTest.java} | 117 +++-----
 .../dialog/ShowRowsDialogCellSelectionTest.java    | 148 +++++++++-
 .../hop/ui/core/dialog/PreviewRowsDialog.java      | 274 +-----------------
 .../apache/hop/ui/core/dialog/ShowRowsDialog.java  | 319 ++-------------------
 .../org/apache/hop/ui/core/widget/TableView.java   | 149 ++++++++--
 .../core/dialog/messages/messages_en_US.properties |   4 +-
 .../core/widget/messages/messages_en_US.properties |   2 +
 7 files changed, 341 insertions(+), 672 deletions(-)

diff --git 
a/rcp/src/test/java/org/apache/hop/ui/core/dialog/ShowRowsDialogCellSelectionTest.java
 
b/rcp/src/test/java/org/apache/hop/ui/core/dialog/SelectRowDialogActivationTest.java
similarity index 50%
copy from 
rcp/src/test/java/org/apache/hop/ui/core/dialog/ShowRowsDialogCellSelectionTest.java
copy to 
rcp/src/test/java/org/apache/hop/ui/core/dialog/SelectRowDialogActivationTest.java
index b5c3b85ce5..e1d74b5e46 100644
--- 
a/rcp/src/test/java/org/apache/hop/ui/core/dialog/ShowRowsDialogCellSelectionTest.java
+++ 
b/rcp/src/test/java/org/apache/hop/ui/core/dialog/SelectRowDialogActivationTest.java
@@ -19,12 +19,12 @@ package org.apache.hop.ui.core.dialog;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
+import org.apache.hop.core.RowMetaAndData;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.row.value.ValueMetaString;
@@ -32,6 +32,7 @@ import org.apache.hop.core.variables.Variables;
 import org.apache.hop.ui.testing.SwtBotTestBase;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Shell;
@@ -43,92 +44,48 @@ import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
 /**
- * Guards in-place cell selection in {@link ShowRowsDialog}: clicking a cell 
must drop a read-only,
- * selectable text field on it that holds the cell's <em>full</em> value, so 
it can be copied even
- * when the grid only shows a truncated, single-lined version.
- *
- * <p>This is the behaviour the transform/hop "output data" preview lost when 
it moved off {@link
- * PreviewRowsDialog} (which has the same click-to-select editor) onto the 
simpler {@code
- * ShowRowsDialog}. Without the editor, the click delivered below leaves no 
{@link Text} on the
- * table and the assertions here fail.
+ * A read-only grid puts a view-only editor on the clicked cell so the full 
value stays selectable.
+ * That editor covers the cell, so the table no longer sees the double-click 
(or Enter) that picks a
+ * row - {@link SelectRowDialog} listens for exactly that. The editor has to 
pass it on.
  */
 @Tag("uitest")
-class ShowRowsDialogCellSelectionTest extends SwtBotTestBase {
-
-  // Longer than the max preview cell length, so the grid truncates it but the 
editor must not.
-  private static final String LONG_VALUE =
-      "a very long value that the preview grid truncates for display but must 
stay selectable "
-          + "in full so it can be copied out of the cell without any 
characters being lost at all";
+class SelectRowDialogActivationTest extends SwtBotTestBase {
 
   @Test
-  void clickingACellDropsAReadOnlyEditorHoldingTheFullValue() {
+  void doubleClickingACellStillPicksTheRow() {
     IRowMeta rowMeta = new RowMeta();
-    rowMeta.addValueMeta(new ValueMetaString("text"));
-    List<Object[]> rows = new ArrayList<>();
-    rows.add(new Object[] {LONG_VALUE});
+    rowMeta.addValueMeta(new ValueMetaString("filename"));
+    List<RowMetaAndData> rows = new ArrayList<>();
+    rows.add(new RowMetaAndData(rowMeta, "first-file.hpl"));
+    rows.add(new RowMetaAndData(rowMeta, "second-file.hpl"));
+
+    AtomicReference<RowMetaAndData> picked = new AtomicReference<>();
 
     withDialog(
-        parent ->
-            new ShowRowsDialog(parent, new Variables(), "Preview", "Output 
rows", rowMeta, rows)
-                .open(),
+        parent -> picked.set(new SelectRowDialog(parent, new Variables(), 
SWT.NONE, rows).open()),
         bot -> {
           Table table = awaitTable(bot);
-          assertNotNull(table, "the ShowRowsDialog table should open");
+          assertNotNull(table, "the SelectRowDialog table should open");
 
+          // First click activates the cell and drops the view-only editor on 
it.
           clickFirstDataCell(table);
-
           Text editor = awaitCellEditor(bot, table);
-          assertNotNull(
-              editor,
-              "clicking a cell should drop a selectable text field holding the 
value in place");
-          assertTrue(
-              (onUi(editor::getStyle) & SWT.READ_ONLY) != 0,
-              "the in-place cell editor must be read-only (a viewer, not an 
editor)");
-          assertEquals(
-              LONG_VALUE,
-              onUi(editor::getText),
-              "the editor must expose the full, untruncated cell value for 
copying");
-          String tooltip = onUi(editor::getToolTipText);
-          assertNotNull(tooltip, "selected cell should show a column-metadata 
tooltip");
-          assertTrue(
-              tooltip.contains("text"),
-              "cell tooltip should include the column name; was: " + tooltip);
-        });
-  }
-
-  @Test
-  void sortingKeepsFullValueLookupForTheSelectedCell() {
-    IRowMeta rowMeta = new RowMeta();
-    rowMeta.addValueMeta(new ValueMetaString("text"));
-    List<Object[]> rows = new ArrayList<>();
-    rows.add(new Object[] {"zebra-" + LONG_VALUE});
-    rows.add(new Object[] {"alpha-" + LONG_VALUE});
-
-    withDialog(
-        parent ->
-            new ShowRowsDialog(parent, new Variables(), "Preview", "Output 
rows", rowMeta, rows)
-                .open(),
-        bot -> {
-          Table table = awaitTable(bot);
-          assertNotNull(table, "the ShowRowsDialog table should open");
+          assertNotNull(editor, "clicking a read-only cell should open the 
view-only editor");
 
-          // Sort by the data column (index 1; 0 is the row-number column).
+          // The second click of the double-click lands on that editor, not on 
the table.
           onUi(
               () -> {
-                table.getColumn(1).notifyListeners(SWT.Selection, new Event());
+                editor.notifyListeners(SWT.MouseDoubleClick, new Event());
                 return null;
               });
-          bot.sleep(100);
-
-          // After ascending sort, "alpha-..." should be first. Click it and 
verify full value.
-          clickFirstDataCell(table);
-          Text editor = awaitCellEditor(bot, table);
-          assertNotNull(editor, "cell editor should open after sort");
-          assertEquals(
-              "alpha-" + LONG_VALUE,
-              onUi(editor::getText),
-              "after sorting, the full buffer value for the clicked row must 
still be resolved");
+          bot.sleep(200);
         });
+
+    assertNotNull(picked.get(), "double-clicking a cell should have picked the 
row");
+    assertEquals(
+        "first-file.hpl",
+        picked.get().getData()[0],
+        "the row under the double-clicked cell should be the one that is 
picked");
   }
 
   /** Fires a left mouse-down at the centre of the first data cell (row 0, 
first value column). */
@@ -146,10 +103,9 @@ class ShowRowsDialogCellSelectionTest extends 
SwtBotTestBase {
         });
   }
 
-  /** Polls for the ShowRowsDialog's table across the open shells. */
   private Table awaitTable(SWTBot bot) {
     for (int attempt = 0; attempt < 40; attempt++) {
-      Table found = onUi(ShowRowsDialogCellSelectionTest::findTableOnUi);
+      Table found = onUi(SelectRowDialogActivationTest::findTableOnUi);
       if (found != null) {
         return found;
       }
@@ -158,10 +114,9 @@ class ShowRowsDialogCellSelectionTest extends 
SwtBotTestBase {
     return null;
   }
 
-  /** Polls for the read-only editor the click drops directly on the table. */
   private Text awaitCellEditor(SWTBot bot, Table table) {
     for (int attempt = 0; attempt < 40; attempt++) {
-      Text found = onUi(() -> findDirectTextChild(table));
+      Text found = onUi(() -> findText(table));
       if (found != null) {
         return found;
       }
@@ -180,12 +135,12 @@ class ShowRowsDialogCellSelectionTest extends 
SwtBotTestBase {
     return null;
   }
 
-  private static Table findTable(org.eclipse.swt.widgets.Composite parent) {
+  private static Table findTable(Composite parent) {
     for (Control child : parent.getChildren()) {
       if (child instanceof Table table) {
         return table;
       }
-      if (child instanceof org.eclipse.swt.widgets.Composite composite) {
+      if (child instanceof Composite composite) {
         Table found = findTable(composite);
         if (found != null) {
           return found;
@@ -195,12 +150,18 @@ class ShowRowsDialogCellSelectionTest extends 
SwtBotTestBase {
     return null;
   }
 
-  /** The cell editor is a {@link Text} parented straight onto the table (via 
a TableEditor). */
-  private static Text findDirectTextChild(Table table) {
-    for (Control child : table.getChildren()) {
+  /** The editor the grid parents onto the table, inside the holder carrying 
the expand icon. */
+  private static Text findText(Composite parent) {
+    for (Control child : parent.getChildren()) {
       if (child instanceof Text text && !text.isDisposed()) {
         return text;
       }
+      if (child instanceof Composite composite) {
+        Text found = findText(composite);
+        if (found != null) {
+          return found;
+        }
+      }
     }
     return null;
   }
diff --git 
a/rcp/src/test/java/org/apache/hop/ui/core/dialog/ShowRowsDialogCellSelectionTest.java
 
b/rcp/src/test/java/org/apache/hop/ui/core/dialog/ShowRowsDialogCellSelectionTest.java
index b5c3b85ce5..9ed71ba4bb 100644
--- 
a/rcp/src/test/java/org/apache/hop/ui/core/dialog/ShowRowsDialogCellSelectionTest.java
+++ 
b/rcp/src/test/java/org/apache/hop/ui/core/dialog/ShowRowsDialogCellSelectionTest.java
@@ -19,6 +19,7 @@ package org.apache.hop.ui.core.dialog;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.ArrayList;
@@ -31,9 +32,13 @@ import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.variables.Variables;
 import org.apache.hop.ui.testing.SwtBotTestBase;
 import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
 import org.eclipse.swt.graphics.Rectangle;
+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.Table;
 import org.eclipse.swt.widgets.TableItem;
@@ -96,6 +101,87 @@ class ShowRowsDialogCellSelectionTest extends 
SwtBotTestBase {
         });
   }
 
+  /**
+   * Copy to clipboard, export to Excel/CSV and save-to-data-set all read the 
cell straight off the
+   * table item, so the item has to hold the complete value: the grid only 
shortens it when drawing.
+   */
+  @Test
+  void gridCellsHoldTheFullValueEvenThoughTheyAreDrawnShortened() {
+    String multiLine = "first line\nsecond line\nthird line";
+
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("text"));
+    rowMeta.addValueMeta(new ValueMetaString("lines"));
+    List<Object[]> rows = new ArrayList<>();
+    rows.add(new Object[] {LONG_VALUE, multiLine});
+
+    withDialog(
+        parent ->
+            new ShowRowsDialog(parent, new Variables(), "Preview", "Output 
rows", rowMeta, rows)
+                .open(),
+        bot -> {
+          Table table = awaitTable(bot);
+          assertNotNull(table, "the ShowRowsDialog table should open");
+
+          assertEquals(
+              LONG_VALUE,
+              onUi(() -> table.getItem(0).getText(1)),
+              "a long cell value must be stored in full, not truncated with an 
ellipsis");
+          assertEquals(
+              multiLine,
+              onUi(() -> table.getItem(0).getText(2)),
+              "a multi-line cell value must keep all of its lines");
+        });
+  }
+
+  /** Ctrl+C on selected rows must put the complete values on the clipboard, 
ellipsis-free. */
+  @Test
+  void copyingSelectedRowsPutsTheFullValuesOnTheClipboard() {
+    String multiLine = "first line\nsecond line";
+
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("text"));
+    rowMeta.addValueMeta(new ValueMetaString("lines"));
+    List<Object[]> rows = new ArrayList<>();
+    rows.add(new Object[] {LONG_VALUE, multiLine});
+
+    withDialog(
+        parent ->
+            new ShowRowsDialog(parent, new Variables(), "Preview", "Output 
rows", rowMeta, rows)
+                .open(),
+        bot -> {
+          Table table = awaitTable(bot);
+          assertNotNull(table, "the ShowRowsDialog table should open");
+
+          // A click makes the row the active one; the key handler ignores 
keys until then.
+          clickFirstDataCell(table);
+
+          String clipped =
+              onUi(
+                  () -> {
+                    table.setSelection(0);
+                    Event event = new Event();
+                    event.keyCode = 'c';
+                    event.stateMask = SWT.MOD1;
+                    table.notifyListeners(SWT.KeyDown, event);
+                    Clipboard clipboard = new Clipboard(display);
+                    try {
+                      return (String) 
clipboard.getContents(TextTransfer.getInstance());
+                    } finally {
+                      clipboard.dispose();
+                    }
+                  });
+
+          assertNotNull(clipped, "Ctrl+C should put the selected row on the 
clipboard");
+          assertTrue(
+              clipped.contains(LONG_VALUE),
+              "the clipboard must hold the full value, not the shortened one; 
was: " + clipped);
+          assertTrue(
+              clipped.contains(multiLine),
+              "the clipboard must keep every line of a multi-line value; was: 
" + clipped);
+        });
+  }
+
   @Test
   void sortingKeepsFullValueLookupForTheSelectedCell() {
     IRowMeta rowMeta = new RowMeta();
@@ -131,6 +217,39 @@ class ShowRowsDialogCellSelectionTest extends 
SwtBotTestBase {
         });
   }
 
+  /**
+   * The in-place editor must be the grid's own one - a text field wrapped in 
a holder that also
+   * carries the "expand" icon opening the multi-line viewer - and not a 
hand-rolled field that only
+   * reacts to a double-click.
+   */
+  @Test
+  void theCellEditorCarriesTheGridsExpandIcon() {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("text"));
+    List<Object[]> rows = new ArrayList<>();
+    rows.add(new Object[] {LONG_VALUE});
+
+    withDialog(
+        parent ->
+            new ShowRowsDialog(parent, new Variables(), "Preview", "Output 
rows", rowMeta, rows)
+                .open(),
+        bot -> {
+          Table table = awaitTable(bot);
+          assertNotNull(table, "the ShowRowsDialog table should open");
+
+          clickFirstDataCell(table);
+
+          Text editor = awaitCellEditor(bot, table);
+          assertNotNull(editor, "clicking a cell should open the grid's 
in-place editor");
+
+          Composite holder = onUi(editor::getParent);
+          assertNotSame(table, holder, "the editor should sit in the holder 
that carries the icon");
+          assertNotNull(
+              onUi(() -> findLabelWithImage(holder)),
+              "the editor holder should carry the expand icon opening the 
multi-line viewer");
+        });
+  }
+
   /** Fires a left mouse-down at the centre of the first data cell (row 0, 
first value column). */
   private void clickFirstDataCell(Table table) {
     onUi(
@@ -180,12 +299,12 @@ class ShowRowsDialogCellSelectionTest extends 
SwtBotTestBase {
     return null;
   }
 
-  private static Table findTable(org.eclipse.swt.widgets.Composite parent) {
+  private static Table findTable(Composite parent) {
     for (Control child : parent.getChildren()) {
       if (child instanceof Table table) {
         return table;
       }
-      if (child instanceof org.eclipse.swt.widgets.Composite composite) {
+      if (child instanceof Composite composite) {
         Table found = findTable(composite);
         if (found != null) {
           return found;
@@ -195,12 +314,31 @@ class ShowRowsDialogCellSelectionTest extends 
SwtBotTestBase {
     return null;
   }
 
-  /** The cell editor is a {@link Text} parented straight onto the table (via 
a TableEditor). */
-  private static Text findDirectTextChild(Table table) {
-    for (Control child : table.getChildren()) {
+  /**
+   * The cell editor is a {@link Text} the grid parents onto the table via a 
TableEditor, wrapped in
+   * a holder composite that also carries the "expand" icon.
+   */
+  private static Text findDirectTextChild(Composite parent) {
+    for (Control child : parent.getChildren()) {
       if (child instanceof Text text && !text.isDisposed()) {
         return text;
       }
+      if (child instanceof Composite composite) {
+        Text found = findDirectTextChild(composite);
+        if (found != null) {
+          return found;
+        }
+      }
+    }
+    return null;
+  }
+
+  /** The expand icon: an image-carrying label next to the editor. */
+  private static Label findLabelWithImage(Composite parent) {
+    for (Control child : parent.getChildren()) {
+      if (child instanceof Label label && label.getImage() != null) {
+        return label;
+      }
     }
     return null;
   }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/dialog/PreviewRowsDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/PreviewRowsDialog.java
index 8d451aca2f..e45997f780 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/PreviewRowsDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/PreviewRowsDialog.java
@@ -40,13 +40,10 @@ import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
-import org.eclipse.swt.custom.TableEditor;
 import org.eclipse.swt.events.KeyAdapter;
 import org.eclipse.swt.events.KeyEvent;
 import org.eclipse.swt.events.KeyListener;
-import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.graphics.Rectangle;
-import org.eclipse.swt.layout.FillLayout;
 import org.eclipse.swt.layout.FormAttachment;
 import org.eclipse.swt.layout.FormData;
 import org.eclipse.swt.layout.FormLayout;
@@ -55,7 +52,6 @@ import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.TableItem;
-import org.eclipse.swt.widgets.Text;
 
 /**
  * Displays rows for transform preview: streaming updates, optional logging 
text, and actions such
@@ -70,11 +66,6 @@ public class PreviewRowsDialog {
       Const.toBoolean(
           
HopConfig.readStringVariable(Const.HOP_BINARY_FIELDS_AVOID_HEX_PREVIEW, 
"false"));
 
-  /** Caps for the floating full-value box: it grows to fit the value, then 
scrolls beyond these. */
-  private static final int MAX_OVERLAY_WIDTH = 600;
-
-  private static final int MAX_OVERLAY_HEIGHT = 400;
-
   private String transformName;
 
   private Label wlFields;
@@ -83,14 +74,6 @@ public class PreviewRowsDialog {
 
   private Shell shell;
 
-  /** Lightweight floating box showing the full value of an overflowing cell 
(Ctrl+Space style). */
-  private Shell valueOverlay;
-
-  /** Read-only text field overlaid on the clicked cell so its value can be 
selected in place. */
-  private TableEditor cellEditor;
-
-  private Text cellEditorText;
-
   private final List<Object[]> buffer;
 
   private String title;
@@ -310,9 +293,9 @@ public class PreviewRowsDialog {
       columns[i].setToolTip(valueMeta.toStringMeta());
       columns[i].setValueMeta(valueMeta);
       columns[i].setImage(GuiResource.getInstance().getImage(valueMeta));
-      // A preview is a viewer: the grid's own editable inline editor must 
stay off. We drop our own
-      // read-only field on the cell instead (see the MouseDown handler 
below), which also frees
-      // double-click to pop up the full, untruncated value of a cell.
+      // A preview is a viewer: the cell may not be edited. The grid gives a 
read-only column a
+      // view-only inline editor with an expand icon, so the full value is 
still selectable in place
+      // and can be opened in the multi-line viewer.
       columns[i].setReadOnly(true);
     }
 
@@ -320,29 +303,10 @@ public class PreviewRowsDialog {
         new TableView(
             variables, shell, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI, 
columns, 0, null, props);
     wFields.setShowingBlueNullValues(true);
-    // Rows are kept in load order so a cell's visual position maps straight 
back to the buffer that
-    // holds its full value. Sorting would reorder items (and sort by the 
truncated display text),
-    // breaking that mapping, so it is disabled here.
+    // Rows stream in and are appended in load order, so sorting them would 
fight with the rows
+    // still arriving. Keep the preview in load order.
     wFields.setSortable(false);
 
-    // Cells only show a truncated, single-lined value for performance. Click 
a cell to drop a
-    // read-only text field on it (like the inline editor of an editable grid) 
so its value can be
-    // selected and copied in place; double-click a cell to see its full, 
original content in a
-    // floating box (handy for long strings, JSON and multi-line values).
-    cellEditor = new TableEditor(wFields.table);
-    cellEditor.grabHorizontal = true;
-    cellEditor.horizontalAlignment = SWT.LEFT;
-    wFields.table.addListener(
-        SWT.MouseDown,
-        event -> {
-          // Leave Shift/Ctrl clicks to the table so row range/toggle 
selection keeps working.
-          if (event.button == 1 && (event.stateMask & (SWT.SHIFT | SWT.MOD1)) 
== 0) {
-            openCellEditor(new Point(event.x, event.y));
-          }
-        });
-    wFields.table.addListener(
-        SWT.MouseDoubleClick, event -> showFullCellValue(new Point(event.x, 
event.y)));
-
     FormData fdFields = new FormData();
     fdFields.left = new FormAttachment(0, 0);
     fdFields.top = new FormAttachment(wlFields, margin);
@@ -358,9 +322,6 @@ public class PreviewRowsDialog {
   }
 
   public void dispose() {
-    if (cellEditor != null) {
-      cellEditor.dispose();
-    }
     PropsUi.getInstance().setScreen(new WindowProperty(shell));
     bounds = shell.getBounds();
     hscroll = wFields.getHorizontalBar().getSelection();
@@ -447,7 +408,9 @@ public class PreviewRowsDialog {
       }
 
       if (show != null) {
-        item.setText(c + 1, TableView.formatCellValueForDisplay(show));
+        // Store the full value: the grid shortens long / multi-line text at 
paint time, so what is
+        // copied, exported or read back out of the table stays complete.
+        item.setText(c + 1, show);
         item.setForeground(c + 1, GuiResource.getInstance().getColorBlack());
       } else {
         // Set null value
@@ -459,227 +422,6 @@ public class PreviewRowsDialog {
     return nrErrors;
   }
 
-  /**
-   * When a data cell is double-clicked, show its full value in a lightweight 
box anchored to the
-   * cell so it can be selected and copied. Overflowing values (long / 
multi-line) get a larger,
-   * scrollable box; a short single value gets a small one. Does nothing for 
non-data cells.
-   */
-  private void showFullCellValue(Point point) {
-    CellRef ref = cellAt(point);
-    if (ref != null) {
-      expandCell(ref.bounds, ref.rowIndex, ref.columnIndex);
-    }
-  }
-
-  /**
-   * Single-click handler: drop a read-only text field on the clicked cell — 
the same cell-fitting
-   * effect as an editable grid's inline editor — holding the full, 
untruncated value so it can be
-   * selected and copied in place. Double-clicking the field expands it to the 
full-value box.
-   */
-  private void openCellEditor(Point point) {
-    CellRef ref = cellAt(point);
-    if (ref == null) {
-      return;
-    }
-    String full = getFullCellString(ref.rowIndex, ref.columnIndex - 1);
-    if (full == null) {
-      return;
-    }
-    TableItem item = wFields.table.getItem(ref.rowIndex);
-
-    if (cellEditorText != null && !cellEditorText.isDisposed()) {
-      cellEditorText.dispose();
-    }
-
-    final Text field = new Text(wFields.table, SWT.SINGLE | SWT.READ_ONLY);
-    PropsUi.setLook(field);
-    field.setText(full);
-    cellEditorText = field;
-    final long openedAt = System.currentTimeMillis();
-
-    // Escape or losing focus removes the field again.
-    field.addListener(
-        SWT.KeyDown,
-        e -> {
-          if (e.keyCode == SWT.ESC) {
-            field.dispose();
-          }
-        });
-    field.addListener(SWT.FocusOut, e -> field.dispose());
-
-    // Double-clicking the field expands to the full-value box. The field is 
created on the first
-    // click, so on some platforms the second click of a double-click lands on 
this fresh field as a
-    // plain MouseDown; treat a click within the OS double-click time of it 
opening as that second
-    // click too. Coordinates are captured so the box anchors to the same cell.
-    final Rectangle cellBounds = ref.bounds;
-    final int rowIndex = ref.rowIndex;
-    final int columnIndex = ref.columnIndex;
-    field.addListener(SWT.MouseDoubleClick, e -> expandCell(cellBounds, 
rowIndex, columnIndex));
-    field.addListener(
-        SWT.MouseDown,
-        e -> {
-          if (System.currentTimeMillis() - openedAt <= 
wFields.getDisplay().getDoubleClickTime()) {
-            expandCell(cellBounds, rowIndex, columnIndex);
-          }
-        });
-
-    cellEditor.setEditor(field, item, columnIndex);
-    field.setFocus();
-    field.selectAll();
-  }
-
-  /** Expand the given cell's full value into the floating, selectable value 
box. */
-  private void expandCell(Rectangle cellBounds, int rowIndex, int columnIndex) 
{
-    if (cellEditorText != null && !cellEditorText.isDisposed()) {
-      cellEditorText.dispose();
-    }
-    // A double-click fires both a MouseDown (caught within the double-click 
window) and a
-    // MouseDoubleClick; ignore the second while the box for this click is 
still up.
-    if (valueOverlay != null && !valueOverlay.isDisposed()) {
-      return;
-    }
-    String full = getFullCellString(rowIndex, columnIndex - 1);
-    if (full != null) {
-      showValueOverlay(cellBounds, full);
-    }
-  }
-
-  /**
-   * Resolve the data cell under a table-relative point, or null when the 
point isn't over a data
-   * cell (the row-number column, empty space, or a row/column outside the 
backing buffer).
-   */
-  private CellRef cellAt(Point point) {
-    if (wFields == null || wFields.isDisposed() || buffer == null || rowMeta 
== null) {
-      return null;
-    }
-    TableItem item = wFields.table.getItem(point);
-    if (item == null) {
-      return null;
-    }
-    int rowIndex = wFields.table.indexOf(item);
-    if (rowIndex < 0 || rowIndex >= buffer.size()) {
-      return null;
-    }
-    // Column 0 is the row-number column; data columns start at 1. Find the 
one under the pointer.
-    for (int i = 1; i < wFields.table.getColumnCount(); i++) {
-      Rectangle b = item.getBounds(i);
-      if (b.contains(point)) {
-        return i - 1 < rowMeta.size() ? new CellRef(rowIndex, i, b) : null;
-      }
-    }
-    return null;
-  }
-
-  /**
-   * A located data cell: its row index into the buffer, its 1-based table 
column, and its bounds.
-   */
-  private static final class CellRef {
-    private final int rowIndex;
-    private final int columnIndex;
-    private final Rectangle bounds;
-
-    private CellRef(int rowIndex, int columnIndex, Rectangle bounds) {
-      this.rowIndex = rowIndex;
-      this.columnIndex = columnIndex;
-      this.bounds = bounds;
-    }
-  }
-
-  /**
-   * Show the full cell value in a lightweight, non-modal multi-line text box 
anchored to the cell —
-   * the same floating-shell idea as the Ctrl+Space variable helper. Dismisses 
on Escape or when it
-   * loses focus.
-   */
-  private void showValueOverlay(Rectangle cellBounds, String value) {
-    if (valueOverlay != null && !valueOverlay.isDisposed()) {
-      valueOverlay.dispose();
-    }
-
-    Point location = wFields.table.toDisplay(cellBounds.x, cellBounds.y);
-
-    // A resizable (but title-less) floating shell: light like the variable 
helper, yet the user can
-    // drag its edges to make it bigger for a long value.
-    final Shell overlay = new Shell(shell, SWT.RESIZE);
-    overlay.setLayout(new FillLayout());
-
-    final Text text =
-        new Text(overlay, SWT.MULTI | SWT.WRAP | SWT.V_SCROLL | SWT.READ_ONLY 
| SWT.BORDER);
-    PropsUi.setLook(text);
-    text.setText(value);
-
-    // Size the box to its content. Width comes from the value's natural 
(unwrapped) width, at least
-    // the cell width and capped so it never sprawls across the screen; height 
is then measured
-    // after
-    // the value wraps to that width (minus the border + scrollbar gutter), so 
a value that spans
-    // several lines gets a taller box instead of a scrollbar — we only scroll 
past the height cap.
-    Point natural = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
-    int contentWidth = Math.min(natural.x + 20, MAX_OVERLAY_WIDTH);
-    int width = Math.max(cellBounds.width, contentWidth);
-    int wrapWidth = Math.max(50, width - 24);
-    Point wrapped = text.computeSize(wrapWidth, SWT.DEFAULT);
-    int contentHeight = Math.min(wrapped.y + 8, MAX_OVERLAY_HEIGHT);
-    int height = Math.max(cellBounds.height + 4, contentHeight);
-    overlay.setSize(width, height);
-    overlay.setLocation(location.x, location.y);
-
-    // Dismiss on Escape.
-    text.addListener(
-        SWT.KeyDown,
-        e -> {
-          if (e.keyCode == SWT.ESC) {
-            overlay.dispose();
-          }
-        });
-
-    overlay.open();
-    valueOverlay = overlay;
-
-    // Grab focus after the current (double-click) event settles, so a 
trailing table focus event
-    // can't immediately close the box. Pre-select the whole value so it's 
ready to copy. Only after
-    // focus is settled inside the box do we arm click-away dismissal — via 
shell deactivation
-    // rather
-    // than a text focus-out, so grabbing a resize edge (which keeps the shell 
active) doesn't close
-    // it.
-    overlay
-        .getDisplay()
-        .asyncExec(
-            () -> {
-              if (text.isDisposed()) {
-                return;
-              }
-              text.setFocus();
-              text.selectAll();
-              overlay.addListener(
-                  SWT.Deactivate,
-                  e -> {
-                    if (!overlay.isDisposed()) {
-                      overlay.dispose();
-                    }
-                  });
-            });
-  }
-
-  /** Convert the raw buffer value at (rowIndex, column) to its full string 
form, no truncation. */
-  private String getFullCellString(int rowIndex, int column) {
-    Object[] row = buffer.get(rowIndex);
-    IValueMeta valueMeta = rowMeta.getValueMeta(column);
-    try {
-      if (valueMeta.isBinary()) {
-        byte[] bytes = valueMeta.getBinary(row[column]);
-        if (bytes == null) {
-          return null;
-        }
-        return PREVIEW_AVOID_BINARY_IN_HEX
-            ? valueMeta.getString(bytes)
-            : Hex.encodeHexString(bytes);
-      }
-      return valueMeta.getString(row[column]);
-    } catch (HopValueException e) {
-      log.logError(Const.getStackTracker(e));
-      return null;
-    }
-  }
-
   private void close() {
     transformName = null;
     dispose();
diff --git a/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowRowsDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowRowsDialog.java
index b62dd882e9..9ce9b421d1 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowRowsDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowRowsDialog.java
@@ -35,10 +35,7 @@ import org.apache.hop.ui.core.gui.WindowProperty;
 import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.TableView;
 import org.eclipse.swt.SWT;
-import org.eclipse.swt.custom.TableEditor;
 import org.eclipse.swt.graphics.Point;
-import org.eclipse.swt.graphics.Rectangle;
-import org.eclipse.swt.layout.FillLayout;
 import org.eclipse.swt.layout.FormAttachment;
 import org.eclipse.swt.layout.FormData;
 import org.eclipse.swt.layout.FormLayout;
@@ -46,7 +43,6 @@ import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.MessageBox;
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.TableItem;
-import org.eclipse.swt.widgets.Text;
 
 /**
  * Read-only dialog that shows a static set of rows with a custom title and 
header message.
@@ -55,9 +51,8 @@ import org.eclipse.swt.widgets.Text;
  * transform preview (streaming, "get more rows", pause/stop, logging text), 
use {@link
  * PreviewRowsDialog} instead.
  *
- * <p>Column headers are sortable. Each table item stores its original buffer 
index via {@link
- * TableItem#setData(Object)} so cell selection and full-value lookup still 
work after a sort
- * ({@link TableView} preserves unkeyed item data when rebuilding rows).
+ * <p>Column headers are sortable. Cells hold their full values — the grid 
only shortens long or
+ * multi-line text when drawing it — so copying, exporting and sorting all 
work on complete values.
  */
 public final class ShowRowsDialog {
 
@@ -70,11 +65,6 @@ public final class ShowRowsDialog {
       Const.toBoolean(
           
HopConfig.readStringVariable(Const.HOP_BINARY_FIELDS_AVOID_HEX_PREVIEW, 
"false"));
 
-  /** Caps for the floating full-value box: it grows to fit the value, then 
scrolls beyond these. */
-  private static final int MAX_OVERLAY_WIDTH = 600;
-
-  private static final int MAX_OVERLAY_HEIGHT = 400;
-
   private final Shell parentShell;
   private final IVariables variables;
   private final String title;
@@ -86,14 +76,6 @@ public final class ShowRowsDialog {
   private TableView tableView;
   private int lineNr;
 
-  /** Read-only text field overlaid on the clicked cell so its value can be 
selected in place. */
-  private TableEditor cellEditor;
-
-  private Text cellEditorText;
-
-  /** Lightweight floating box showing the full value of an overflowing cell 
(Ctrl+Space style). */
-  private Shell valueOverlay;
-
   public ShowRowsDialog(
       Shell parent,
       IVariables variables,
@@ -149,7 +131,7 @@ public final class ShowRowsDialog {
 
     tableView = buildTableView(margin, messageLabel);
     populateRows();
-    setupCellSelection();
+    setupCellTooltip();
 
     BaseDialog.defaultShellHandling(shell, c -> close(), c -> close());
   }
@@ -163,6 +145,8 @@ public final class ShowRowsDialog {
       columns[i].setToolTip(formatColumnMetaTooltip(valueMeta));
       columns[i].setValueMeta(valueMeta);
       columns[i].setImage(GuiResource.getInstance().getImage(valueMeta));
+      // Read-only: the grid gives such a column a view-only inline editor 
with an expand icon, so
+      // the full value stays selectable in place and can be opened in the 
multi-line viewer.
       columns[i].setReadOnly(true);
     }
 
@@ -176,8 +160,7 @@ public final class ShowRowsDialog {
             null,
             PropsUi.getInstance());
     view.setShowingBlueNullValues(true);
-    // Column sorting is enabled; each item stores its original buffer index 
so full-value lookup
-    // still works after the table is reordered (see resolveBufferIndex).
+    // Column sorting is enabled: items carry their full values, so a sort 
reorders complete rows.
     view.setSortable(true);
 
     FormData fdTable = new FormData();
@@ -198,21 +181,18 @@ public final class ShowRowsDialog {
       } else {
         item = new TableItem(tableView.table, SWT.NONE);
       }
-      fillRow(item, rows.get(i), i);
+      fillRow(item, rows.get(i));
     }
     if (!tableView.isDisposed()) {
       tableView.optWidth(true, 200);
     }
   }
 
-  private void fillRow(TableItem item, Object[] row, int bufferIndex) {
+  private void fillRow(TableItem item, Object[] row) {
     if (row == null) {
       return;
     }
 
-    // Unkeyed data: TableView preserves getData()/setData(Object) across 
column sorts.
-    item.setData(bufferIndex);
-
     lineNr++;
     String rowNumber;
     try {
@@ -246,7 +226,9 @@ public final class ShowRowsDialog {
       }
 
       if (displayValue != null) {
-        item.setText(column + 1, 
TableView.formatCellValueForDisplay(displayValue));
+        // Store the full value: the grid shortens long / multi-line text at 
paint time, so what is
+        // copied, exported or read back out of the table stays complete.
+        item.setText(column + 1, displayValue);
         item.setForeground(column + 1, 
GuiResource.getInstance().getColorBlack());
       } else {
         item.setText(column + 1, "<null>");
@@ -259,42 +241,22 @@ public final class ShowRowsDialog {
     if (shell == null || shell.isDisposed()) {
       return;
     }
-    if (cellEditor != null) {
-      cellEditor.dispose();
-    }
     PropsUi.getInstance().setScreen(new WindowProperty(shell));
     shell.dispose();
   }
 
   /**
-   * Cells only show a truncated, single-lined value for performance (see 
{@link
-   * TableView#formatCellValueForDisplay}). Clicking a cell drops a read-only 
text field on it (like
-   * the inline editor of an editable grid) so its full value can be selected 
and copied in place;
-   * double-clicking a cell shows its full, original content in a floating box 
(handy for long
-   * strings, JSON and multi-line values). Hovering (or selecting) a cell 
shows column metadata in a
-   * tooltip: name, type, length, precision.
+   * Hovering a cell shows column metadata in a tooltip: name, type, length, 
precision. Selecting a
+   * cell is handled by the grid itself: a read-only column gets a view-only 
inline editor holding
+   * the full value, with an expand icon for the multi-line viewer.
    */
-  private void setupCellSelection() {
-    cellEditor = new TableEditor(tableView.table);
-    cellEditor.grabHorizontal = true;
-    cellEditor.horizontalAlignment = SWT.LEFT;
-    tableView.table.addListener(
-        SWT.MouseDown,
-        event -> {
-          // Leave Shift/Ctrl clicks to the table so row range/toggle 
selection keeps working.
-          if (event.button == 1 && (event.stateMask & (SWT.SHIFT | SWT.MOD1)) 
== 0) {
-            openCellEditor(new Point(event.x, event.y));
-          }
-        });
-    tableView.table.addListener(
-        SWT.MouseDoubleClick, event -> showFullCellValue(new Point(event.x, 
event.y)));
-    // Dynamic cell tooltip with column metadata (name, type, length, 
precision, ...).
+  private void setupCellTooltip() {
     tableView.table.addListener(
         SWT.MouseMove,
         event -> {
-          CellRef ref = cellAt(new Point(event.x, event.y));
+          int dataColumn = dataColumnAt(new Point(event.x, event.y));
           String tip =
-              ref == null ? null : 
formatColumnMetaTooltip(rowMeta.getValueMeta(ref.dataColumn));
+              dataColumn < 0 ? null : 
formatColumnMetaTooltip(rowMeta.getValueMeta(dataColumn));
           if (!Objects.equals(tip, tableView.table.getToolTipText())) {
             tableView.table.setToolTipText(tip);
           }
@@ -339,254 +301,25 @@ public final class ShowRowsDialog {
     return tip.toString();
   }
 
-  private void showFullCellValue(Point point) {
-    CellRef ref = cellAt(point);
-    if (ref != null) {
-      expandCell(ref.bounds, ref.bufferIndex, ref.tableColumn);
-    }
-  }
-
-  /**
-   * Single-click handler: drop a read-only text field on the clicked cell — 
the same cell-fitting
-   * effect as an editable grid's inline editor — holding the full, 
untruncated value so it can be
-   * selected and copied in place. Double-clicking the field expands it to the 
full-value box.
-   */
-  private void openCellEditor(Point point) {
-    CellRef ref = cellAt(point);
-    if (ref == null) {
-      return;
-    }
-    String full = getFullCellString(ref.bufferIndex, ref.dataColumn);
-    if (full == null) {
-      return;
-    }
-    TableItem item = tableView.table.getItem(ref.tableRowIndex);
-
-    if (cellEditorText != null && !cellEditorText.isDisposed()) {
-      cellEditorText.dispose();
-    }
-
-    final Text field = new Text(tableView.table, SWT.SINGLE | SWT.READ_ONLY);
-    PropsUi.setLook(field);
-    field.setText(full);
-    
field.setToolTipText(formatColumnMetaTooltip(rowMeta.getValueMeta(ref.dataColumn)));
-    cellEditorText = field;
-    final long openedAt = System.currentTimeMillis();
-
-    // Escape or losing focus removes the field again.
-    field.addListener(
-        SWT.KeyDown,
-        e -> {
-          if (e.keyCode == SWT.ESC) {
-            field.dispose();
-          }
-        });
-    field.addListener(SWT.FocusOut, e -> field.dispose());
-
-    // Double-clicking the field expands to the full-value box. The field is 
created on the first
-    // click, so on some platforms the second click of a double-click lands on 
this fresh field as a
-    // plain MouseDown; treat a click within the OS double-click time of it 
opening as that second
-    // click too. Coordinates are captured so the box anchors to the same cell.
-    final Rectangle cellBounds = ref.bounds;
-    final int bufferIndex = ref.bufferIndex;
-    final int tableColumn = ref.tableColumn;
-    field.addListener(SWT.MouseDoubleClick, e -> expandCell(cellBounds, 
bufferIndex, tableColumn));
-    field.addListener(
-        SWT.MouseDown,
-        e -> {
-          if (System.currentTimeMillis() - openedAt
-              <= tableView.getDisplay().getDoubleClickTime()) {
-            expandCell(cellBounds, bufferIndex, tableColumn);
-          }
-        });
-
-    cellEditor.setEditor(field, item, tableColumn);
-    field.setFocus();
-    field.selectAll();
-  }
-
-  /** Expand the given cell's full value into the floating, selectable value 
box. */
-  private void expandCell(Rectangle cellBounds, int bufferIndex, int 
tableColumn) {
-    if (cellEditorText != null && !cellEditorText.isDisposed()) {
-      cellEditorText.dispose();
-    }
-    // A double-click fires both a MouseDown (caught within the double-click 
window) and a
-    // MouseDoubleClick; ignore the second while the box for this click is 
still up.
-    if (valueOverlay != null && !valueOverlay.isDisposed()) {
-      return;
-    }
-    String full = getFullCellString(bufferIndex, tableColumn - 1);
-    if (full != null) {
-      showValueOverlay(cellBounds, full);
-    }
-  }
-
   /**
-   * Resolve the data cell under a table-relative point, or null when the 
point isn't over a data
-   * cell (the row-number column, empty space, or a row/column outside the 
backing buffer).
+   * The 0-based data column under a table-relative point, or -1 when the 
point isn't over a data
+   * cell (the row-number column, empty space, or a column outside the row 
metadata).
    */
-  private CellRef cellAt(Point point) {
-    if (tableView == null || tableView.isDisposed() || rows == null || rowMeta 
== null) {
-      return null;
+  private int dataColumnAt(Point point) {
+    if (tableView == null || tableView.isDisposed() || rowMeta == null) {
+      return -1;
     }
     TableItem item = tableView.table.getItem(point);
     if (item == null) {
-      return null;
-    }
-    int tableRowIndex = tableView.table.indexOf(item);
-    if (tableRowIndex < 0) {
-      return null;
-    }
-    int bufferIndex = resolveBufferIndex(item);
-    if (bufferIndex < 0 || bufferIndex >= rows.size()) {
-      return null;
+      return -1;
     }
     // Column 0 is the row-number column; data columns start at 1. Find the 
one under the pointer.
     for (int i = 1; i < tableView.table.getColumnCount(); i++) {
-      Rectangle b = item.getBounds(i);
-      if (b.contains(point)) {
+      if (item.getBounds(i).contains(point)) {
         int dataColumn = i - 1;
-        return dataColumn < rowMeta.size()
-            ? new CellRef(bufferIndex, tableRowIndex, i, dataColumn, b)
-            : null;
+        return dataColumn < rowMeta.size() ? dataColumn : -1;
       }
     }
-    return null;
-  }
-
-  /**
-   * Map a visual table item back to its original {@link #rows} index. Prefers 
the value stored at
-   * fill time via {@link TableItem#setData(Object)}; falls back to the 
1-based line number in
-   * column 0, which also moves with the row when the table is sorted.
-   */
-  private int resolveBufferIndex(TableItem item) {
-    Object data = item.getData();
-    if (data instanceof Integer index) {
-      return index;
-    }
-    // Fallback: original line number text in the # column (1-based).
-    try {
-      String text = item.getText(0);
-      if (Utils.isEmpty(text)) {
-        return -1;
-      }
-      return Integer.parseInt(text.trim()) - 1;
-    } catch (NumberFormatException e) {
-      return -1;
-    }
-  }
-
-  /**
-   * A located data cell: buffer index into {@link #rows}, visual table row, 
1-based table column,
-   * 0-based data column, and cell bounds.
-   */
-  private static final class CellRef {
-    private final int bufferIndex;
-    private final int tableRowIndex;
-    private final int tableColumn;
-    private final int dataColumn;
-    private final Rectangle bounds;
-
-    private CellRef(
-        int bufferIndex, int tableRowIndex, int tableColumn, int dataColumn, 
Rectangle bounds) {
-      this.bufferIndex = bufferIndex;
-      this.tableRowIndex = tableRowIndex;
-      this.tableColumn = tableColumn;
-      this.dataColumn = dataColumn;
-      this.bounds = bounds;
-    }
-  }
-
-  /**
-   * Show the full cell value in a lightweight, non-modal multi-line text box 
anchored to the cell —
-   * the same floating-shell idea as the Ctrl+Space variable helper. Dismisses 
on Escape or when it
-   * loses focus.
-   */
-  private void showValueOverlay(Rectangle cellBounds, String value) {
-    if (valueOverlay != null && !valueOverlay.isDisposed()) {
-      valueOverlay.dispose();
-    }
-
-    Point location = tableView.table.toDisplay(cellBounds.x, cellBounds.y);
-
-    // A resizable (but title-less) floating shell: light like the variable 
helper, yet the user can
-    // drag its edges to make it bigger for a long value.
-    final Shell overlay = new Shell(shell, SWT.RESIZE);
-    overlay.setLayout(new FillLayout());
-
-    final Text text =
-        new Text(overlay, SWT.MULTI | SWT.WRAP | SWT.V_SCROLL | SWT.READ_ONLY 
| SWT.BORDER);
-    PropsUi.setLook(text);
-    text.setText(value);
-
-    // Size the box to its content. Width comes from the value's natural 
(unwrapped) width, at least
-    // the cell width and capped so it never sprawls across the screen; height 
is then measured
-    // after
-    // the value wraps to that width (minus the border + scrollbar gutter), so 
a value that spans
-    // several lines gets a taller box instead of a scrollbar — we only scroll 
past the height cap.
-    Point natural = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
-    int contentWidth = Math.min(natural.x + 20, MAX_OVERLAY_WIDTH);
-    int width = Math.max(cellBounds.width, contentWidth);
-    int wrapWidth = Math.max(50, width - 24);
-    Point wrapped = text.computeSize(wrapWidth, SWT.DEFAULT);
-    int contentHeight = Math.min(wrapped.y + 8, MAX_OVERLAY_HEIGHT);
-    int height = Math.max(cellBounds.height + 4, contentHeight);
-    overlay.setSize(width, height);
-    overlay.setLocation(location.x, location.y);
-
-    // Dismiss on Escape.
-    text.addListener(
-        SWT.KeyDown,
-        e -> {
-          if (e.keyCode == SWT.ESC) {
-            overlay.dispose();
-          }
-        });
-
-    overlay.open();
-    valueOverlay = overlay;
-
-    // Grab focus after the current (double-click) event settles, so a 
trailing table focus event
-    // can't immediately close the box. Pre-select the whole value so it's 
ready to copy. Only after
-    // focus is settled inside the box do we arm click-away dismissal — via 
shell deactivation
-    // rather
-    // than a text focus-out, so grabbing a resize edge (which keeps the shell 
active) doesn't close
-    // it.
-    overlay
-        .getDisplay()
-        .asyncExec(
-            () -> {
-              if (text.isDisposed()) {
-                return;
-              }
-              text.setFocus();
-              text.selectAll();
-              overlay.addListener(
-                  SWT.Deactivate,
-                  e -> {
-                    if (!overlay.isDisposed()) {
-                      overlay.dispose();
-                    }
-                  });
-            });
-  }
-
-  /** Convert the raw buffer value at (rowIndex, column) to its full string 
form, no truncation. */
-  private String getFullCellString(int rowIndex, int column) {
-    Object[] row = rows.get(rowIndex);
-    IValueMeta valueMeta = rowMeta.getValueMeta(column);
-    try {
-      if (valueMeta.isBinary()) {
-        byte[] bytes = valueMeta.getBinary(row[column]);
-        if (bytes == null) {
-          return null;
-        }
-        return AVOID_BINARY_IN_HEX ? valueMeta.getString(bytes) : 
Hex.encodeHexString(bytes);
-      }
-      return valueMeta.getString(row[column]);
-    } catch (HopValueException e) {
-      new LogChannel(PKG).logError("Unable to format cell value", e);
-      return null;
-    }
+    return -1;
   }
 }
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 3564dbd8fb..e12864812b 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
@@ -841,6 +841,10 @@ public class TableView extends Composite {
       }
       final int rowNr = table.indexOf(row);
       final int colNr = activeTableColumn;
+      if (isColumnReadOnly(colNr)) {
+        // Nothing is typed into a view-only editor, so there is nothing to 
write back.
+        return;
+      }
       final String value = getTextWidgetValue(colNr);
 
       final String[] fBeforeEdit = beforeEdit;
@@ -1295,6 +1299,12 @@ public class TableView extends Composite {
         final Control ftext = text;
         final Composite fholder = inlineEditorHolder;
 
+        // The editor on a read-only cell is a viewer: it just goes away, it 
never writes back.
+        if (isColumnReadOnly(colNr)) {
+          disposeInlineEditor(ftext, fholder);
+          return;
+        }
+
         final String[] fBeforeEdit = beforeEdit;
 
         // Save the position of the caret for the focus-dropping popup-dialogs
@@ -1712,8 +1722,8 @@ public class TableView extends Composite {
       final IRowMeta sourceRowMeta = buildTableSourceRowMeta(rowMeta, 
conversionRowMeta);
       List<Object[]> v = getTableItemsAsRows(items, sourceRowMeta);
 
-      // Keep TableItem#getData() across the rebuild so callers (e.g. 
ShowRowsDialog) can map a
-      // visual row back to an external buffer index after the user sorts a 
column.
+      // Keep TableItem#getData() across the rebuild so callers that tag their 
rows can still map a
+      // visual row back to their own data after the user sorts a column.
       final Object[] preservedData = new Object[items.length];
       for (int i = 0; i < items.length; i++) {
         preservedData[i] = items[i].getData();
@@ -1894,6 +1904,14 @@ public class TableView extends Composite {
     disposeInlineEditor(text, inlineEditorHolder);
   }
 
+  /** True when the given (1-based) table column is read-only, so its editor 
is view-only. */
+  private boolean isColumnReadOnly(int colNr) {
+    return colNr > 0
+        && colNr - 1 < columns.length
+        && columns[colNr - 1] != null
+        && columns[colNr - 1].isReadOnly();
+  }
+
   /** Variant for call sites that captured the editor and its holder before 
going asynchronous. */
   @SuppressWarnings("javabugs:S2259") // the holder is created before it is 
deferred for disposal
   private void disposeInlineEditor(Control editorControl, Composite holder) {
@@ -1926,6 +1944,12 @@ public class TableView extends Composite {
     if (text == null || text.isDisposed()) {
       return;
     }
+    // The editor on a read-only cell is a viewer: take it down again without 
touching the value.
+    if (isColumnReadOnly(colNr)) {
+      disposeInlineEditor();
+      table.setFocus();
+      return;
+    }
     String textData = getTextWidgetValue(colNr);
 
     row.setText(colNr, textData);
@@ -2022,11 +2046,12 @@ public class TableView extends Composite {
 
   /**
    * 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.
+   * the inline editor. On a read-only column it opens as a value viewer. 
Silently does nothing for
+   * cells that have no pop-out (combo, button, password or disabled), so it 
is safe to wire to a
+   * key stroke that is not cell-aware.
    */
   private void expandActiveCell() {
-    if (readonly || !table.isEnabled() || !isEnabled() || columns.length == 0) 
{
+    if (!table.isEnabled() || !isEnabled() || columns.length == 0) {
       return;
     }
     if (activeTableItem == null || activeTableItem.isDisposed()) {
@@ -2041,7 +2066,6 @@ public class TableView extends Composite {
     if (colinfo == null
         || (colinfo.getType() != ColumnInfo.COLUMN_TYPE_TEXT
             && colinfo.getType() != ColumnInfo.COLUMN_TYPE_TEXT_BUTTON)
-        || colinfo.isReadOnly()
         || colinfo.isPasswordField()) {
       return;
     }
@@ -2064,6 +2088,9 @@ public class TableView extends Composite {
       multilineShell.setFocus();
       return;
     }
+    // On a read-only column the pop-out is a value viewer: same box, but it 
can't be typed in and
+    // it never writes back to the cell.
+    final boolean viewOnly = colinfo.isReadOnly();
     // If an inline editor is already open on this cell, carry over its 
current (possibly edited)
     // text; otherwise start from the stored cell value. Read it before 
disposing the inline editor.
     String seed;
@@ -2077,9 +2104,11 @@ public class TableView extends Composite {
     setPosition(rowNr, colNr);
     table.setSelection(new TableItem[] {row});
 
-    beforeEdit = getItemText(row);
-    // An edit is already in progress when the carried-over text differs from 
the stored value.
-    fieldChanged = !seed.equals(row.getText(colNr));
+    if (!viewOnly) {
+      beforeEdit = getItemText(row);
+      // An edit is already in progress when the carried-over text differs 
from the stored value.
+      fieldChanged = !seed.equals(row.getText(colNr));
+    }
 
     Rectangle cellBounds = row.getBounds(colNr);
     Point location = table.toDisplay(cellBounds.x, cellBounds.y);
@@ -2097,20 +2126,34 @@ public class TableView extends Composite {
         });
     popup.setLayout(new FillLayout());
 
-    final Text multi = new Text(popup, SWT.MULTI | SWT.WRAP | SWT.V_SCROLL | 
SWT.BORDER);
+    final Text multi =
+        new Text(
+            popup,
+            SWT.MULTI
+                | SWT.WRAP
+                | SWT.V_SCROLL
+                | SWT.BORDER
+                | (viewOnly ? SWT.READ_ONLY : SWT.NONE));
     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"));
+    multi.setToolTipText(
+        BaseMessages.getString(
+            PKG,
+            viewOnly
+                ? "TableView.tooltip.MultilineViewerKeys"
+                : "TableView.tooltip.MultilineEditorKeys"));
     // Seed with the platform delimiter so line breaks render (Windows needs 
\r\n).
     multi.setText(toPlatformLineBreaks(seed));
 
     // Track changes for undo + modified notifications, exactly like the 
inline editors do.
-    multi.addModifyListener(lsUndo);
-    if (lsMod != null) {
-      multi.addModifyListener(lsMod);
-    }
-    if (colinfo.isUsingVariables()) {
-      multi.addKeyListener(new ControlSpaceKeyAdapter(variables, multi));
+    if (!viewOnly) {
+      multi.addModifyListener(lsUndo);
+      if (lsMod != null) {
+        multi.addModifyListener(lsMod);
+      }
+      if (colinfo.isUsingVariables()) {
+        multi.addKeyListener(new ControlSpaceKeyAdapter(variables, multi));
+      }
     }
 
     popup.setSize(Math.max(cellBounds.width, 400), 200);
@@ -2129,6 +2172,10 @@ public class TableView extends Composite {
           if (!table.isDisposed()) {
             table.setFocus();
           }
+          if (viewOnly) {
+            // A value viewer only closes: nothing to store, nothing changed.
+            return;
+          }
           if (!newValue.equals(row.getText(colNr))) {
             row.setText(colNr, newValue);
           }
@@ -2977,7 +3024,12 @@ public class TableView extends Composite {
 
     ColumnInfo colinfo = columns[colNr - 1];
 
-    if (colinfo.isReadOnly()) {
+    // A read-only cell can't be edited, but its value still has to be 
readable and selectable: it
+    // is drawn shortened and single-lined, so without an editor there is no 
way to see the rest of
+    // it. Give it the same inline editor (and expand icon), only view-only. 
Columns that hand their
+    // click to a selection adapter keep doing nothing, as before.
+    final boolean viewOnly = colinfo.isReadOnly();
+    if (viewOnly && colinfo.getSelectionAdapter() != null) {
       return;
     }
 
@@ -3010,10 +3062,10 @@ public class TableView extends Composite {
       return;
     }
 
-    String content = row.getText(colNr) + (extra != 0 ? "" + extra : "");
+    String content = row.getText(colNr) + (!viewOnly && extra != 0 ? "" + 
extra : "");
     String tooltip = columns[colNr - 1].getToolTip();
 
-    final boolean useVariables = columns[colNr - 1].isUsingVariables();
+    final boolean useVariables = !viewOnly && columns[colNr - 
1].isUsingVariables();
     final boolean passwordField = columns[colNr - 1].isPasswordField();
 
     final ModifyListener modifyListener = me -> 
setColumnWidthBasedOnTextField(colNr, useVariables);
@@ -3101,17 +3153,23 @@ public class TableView extends Composite {
         textWidget.addListener(SWT.KeyUp, lsKeyUp);
       }
     } else {
-      Text textWidget = new Text(editorParent, SWT.NONE);
+      Text textWidget = new Text(editorParent, viewOnly ? SWT.READ_ONLY : 
SWT.NONE);
       text = textWidget;
       textWidget.setText(content);
-      if (lsMod != null) {
-        textWidget.addModifyListener(lsMod);
+      // A view-only editor never changes anything, so it takes no modify/undo 
listeners.
+      if (!viewOnly) {
+        if (lsMod != null) {
+          textWidget.addModifyListener(lsMod);
+        }
+        textWidget.addModifyListener(lsUndo);
+        // Make the column larger so we can still see the string we're 
entering...
+        textWidget.addModifyListener(modifyListener);
       }
-      textWidget.addModifyListener(lsUndo);
       textWidget.setSelection(content.length());
+      if (viewOnly) {
+        forwardRowActivation(textWidget);
+      }
       textWidget.addKeyListener(lsKeyText);
-      // Make the column larger so we can still see the string we're 
entering...
-      textWidget.addModifyListener(modifyListener);
       if (selectText) {
         textWidget.selectAll();
       }
@@ -3145,6 +3203,33 @@ public class TableView extends Composite {
     editor.layout();
   }
 
+  /**
+   * A view-only editor covers the cell, so the table itself never sees the 
second click of a
+   * double-click, nor the Enter key, that would otherwise fire {@link 
SWT#DefaultSelection} and
+   * activate the row. Pass those on, so dialogs that pick a row that way 
(e.g. SelectRowDialog)
+   * keep working on a read-only grid. Deferred, because the editor's own 
handling of the key takes
+   * it down first.
+   */
+  private void forwardRowActivation(Text textWidget) {
+    Listener activateRow =
+        e ->
+            getDisplay()
+                .asyncExec(
+                    () -> {
+                      if (!table.isDisposed()) {
+                        table.notifyListeners(SWT.DefaultSelection, new 
Event());
+                      }
+                    });
+    textWidget.addListener(SWT.MouseDoubleClick, activateRow);
+    textWidget.addListener(
+        SWT.KeyDown,
+        e -> {
+          if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
+            activateRow.handleEvent(e);
+          }
+        });
+  }
+
   /**
    * 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
@@ -3156,7 +3241,12 @@ public class TableView extends Composite {
     Label expandLabel = new Label(holder, SWT.NONE);
     PropsUi.setLook(expandLabel);
     expandLabel.setImage(GuiResource.getInstance().getImageMaximizePanel());
-    expandLabel.setToolTipText(BaseMessages.getString(PKG, 
"TableView.tooltip.ExpandValue"));
+    expandLabel.setToolTipText(
+        BaseMessages.getString(
+            PKG,
+            colinfo.isReadOnly()
+                ? "TableView.tooltip.ViewValue"
+                : "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
@@ -3496,7 +3586,10 @@ public class TableView extends Composite {
         for (int r = 0; r < table.getItemCount() && (r < nrLines || nrLines <= 
0); r++) {
           TableItem ti = table.getItem(r);
           if (ti != null) {
-            columnStrings.add(ti.getText(c));
+            // Size on what is actually drawn: a long or multi-line value is 
shown shortened, so
+            // measuring the full value would stretch the column for text 
nobody sees.
+            String display = customCellText(ti, c);
+            columnStrings.add(display != null ? display : ti.getText(c));
           }
         }
       }
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
index 4589d50e52..9701ebb5c2 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
@@ -194,9 +194,9 @@ EnterOptionsDialog.ShowCanvasGrid.ToolTip=If enabled, the 
canvas grid will be vi
 EnterOptionsDialog.ShowTableViewToolbar.Label=Show toolbar
 EnterOptionsDialog.ShowTableViewToolbar.ToolTip=Enabling this option makes a 
toolbar appear above all tables in the Hop GUI.
 EnterOptionsDialog.MaxPreviewCellLength.Label=Maximum grid cell length
-EnterOptionsDialog.MaxPreviewCellLength.ToolTip=The maximum number of 
characters shown in a single grid cell. Longer values are truncated for display 
(double-click the cell to see or edit the full value). Set to 0 to disable 
truncation. In the web version this applies to preview grids only.
+EnterOptionsDialog.MaxPreviewCellLength.ToolTip=The maximum number of 
characters shown in a single grid cell. Longer values are only shortened for 
display\: click a cell to see its full value in place, or use the expand icon 
to open it in the multi-line editor. Copying, exporting and saving always use 
the full value. Set to 0 to disable shortening. This has no effect in the web 
version.
 EnterOptionsDialog.ShowPreviewLineBreaks.Label=Show line breaks as symbols in 
grid cells
-EnterOptionsDialog.ShowPreviewLineBreaks.ToolTip=When enabled, line breaks and 
tabs in grid cells are shown as symbols on a single line. When disabled 
(default), a value that contains a line break is cut at the first break and 
marked with an ellipsis. Double-click the cell to see or edit the full value 
either way. In the web version this applies to preview grids only.
+EnterOptionsDialog.ShowPreviewLineBreaks.ToolTip=When enabled, line breaks and 
tabs in grid cells are shown as symbols on a single line. When disabled 
(default), a value that contains a line break is cut at the first break and 
marked with an ellipsis. Either way this only affects how the cell is drawn\: 
click a cell to see its full value, and copying or exporting keeps every line 
break. This has no effect in the web version.
 EnterOptionsDialog.ShowViewport.Label=Show viewport
 EnterOptionsDialog.ShowViewport.ToolTip=If enabled, the viewport in the bottom 
right will be shown
 EnterOptionsDialog.SortFieldByName.Label=Sort field names alphabetically in 
dropdowns
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 5ab1ef7367..27428e920e 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
@@ -97,7 +97,9 @@ 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.ViewValue=Show the full value in a multi-line viewer 
(Shift+Enter)
 TableView.tooltip.MultilineEditorKeys=Enter\: save and close - Shift+Enter\: 
new line - Esc\: cancel
+TableView.tooltip.MultilineViewerKeys=Enter or Esc\: close - the value is 
read-only
 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