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 e60021c8e2 add ability to select values in the showRowsDialog, fixes 
#7444 (#7618)
e60021c8e2 is described below

commit e60021c8e296c6cdcfa9eb9ec6f88d5c0464ddc8
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Jul 24 13:08:42 2026 +0200

    add ability to select values in the showRowsDialog, fixes #7444 (#7618)
---
 .../dialog/ShowRowsDialogCellSelectionTest.java    | 185 ++++++++++++++
 .../apache/hop/ui/core/dialog/ShowRowsDialog.java  | 265 ++++++++++++++++++++-
 2 files changed, 449 insertions(+), 1 deletion(-)

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
new file mode 100644
index 0000000000..202bac6b54
--- /dev/null
+++ 
b/rcp/src/test/java/org/apache/hop/ui/core/dialog/ShowRowsDialogCellSelectionTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.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.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+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.graphics.Rectangle;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+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;
+
+/**
+ * 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.
+ */
+@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";
+
+  @Test
+  void clickingACellDropsAReadOnlyEditorHoldingTheFullValue() {
+    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 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");
+        });
+  }
+
+  /** Fires a left mouse-down at the centre of the first data cell (row 0, 
first value column). */
+  private void clickFirstDataCell(Table table) {
+    onUi(
+        () -> {
+          TableItem item = table.getItem(0);
+          Rectangle bounds = item.getBounds(1); // column 0 is the row number
+          Event event = new Event();
+          event.button = 1;
+          event.x = bounds.x + bounds.width / 2;
+          event.y = bounds.y + bounds.height / 2;
+          table.notifyListeners(SWT.MouseDown, event);
+          return null;
+        });
+  }
+
+  /** 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);
+      if (found != null) {
+        return found;
+      }
+      bot.sleep(50);
+    }
+    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));
+      if (found != null) {
+        return found;
+      }
+      bot.sleep(50);
+    }
+    return null;
+  }
+
+  private static Table findTableOnUi() {
+    for (Shell openShell : display.getShells()) {
+      Table found = findTable(openShell);
+      if (found != null) {
+        return found;
+      }
+    }
+    return null;
+  }
+
+  private static Table findTable(org.eclipse.swt.widgets.Composite parent) {
+    for (Control child : parent.getChildren()) {
+      if (child instanceof Table table) {
+        return table;
+      }
+      if (child instanceof org.eclipse.swt.widgets.Composite composite) {
+        Table found = findTable(composite);
+        if (found != null) {
+          return found;
+        }
+      }
+    }
+    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()) {
+      if (child instanceof Text text && !text.isDisposed()) {
+        return text;
+      }
+    }
+    return null;
+  }
+
+  /** 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();
+  }
+}
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 bbd246ed11..b6dab857de 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
@@ -34,6 +34,10 @@ 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;
@@ -41,6 +45,7 @@ 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.
@@ -60,6 +65,11 @@ 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;
@@ -71,6 +81,14 @@ 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,
@@ -126,6 +144,7 @@ public final class ShowRowsDialog {
 
     tableView = buildTableView(margin, messageLabel);
     populateRows();
+    setupCellSelection();
 
     BaseDialog.defaultShellHandling(shell, c -> close(), c -> close());
   }
@@ -152,7 +171,10 @@ public final class ShowRowsDialog {
             null,
             PropsUi.getInstance());
     view.setShowingBlueNullValues(true);
-    view.setSortable(true);
+    // Rows are kept in load order so a cell's visual position maps straight 
back to the buffer that
+    // holds its full value (see getFullCellString). Sorting rebuilds the 
table items from their
+    // truncated display text, breaking that mapping, so it is disabled here.
+    view.setSortable(false);
 
     FormData fdTable = new FormData();
     fdTable.left = new FormAttachment(0, 0);
@@ -230,7 +252,248 @@ 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).
+   */
+  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)));
+  }
+
+  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 = tableView.table.getItem(ref.rowIndex);
+
+    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);
+    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
+              <= tableView.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 (tableView == null || tableView.isDisposed() || rows == null || rowMeta 
== null) {
+      return null;
+    }
+    TableItem item = tableView.table.getItem(point);
+    if (item == null) {
+      return null;
+    }
+    int rowIndex = tableView.table.indexOf(item);
+    if (rowIndex < 0 || rowIndex >= rows.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 < tableView.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 = 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;
+    }
+  }
 }

Reply via email to