This is an automated email from the ASF dual-hosted git repository.

mattcasters 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 3501584eb9 Tableview improvements, fixes  #7404 (#7407)
3501584eb9 is described below

commit 3501584eb9995c14eaa3811a0377ccf0aae23148
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Jul 2 17:21:40 2026 +0200

    Tableview improvements, fixes  #7404 (#7407)
---
 .../main/java/org/apache/hop/ui/core/PropsUi.java  |  32 ++
 .../hop/ui/core/dialog/PreviewRowsDialog.java      | 140 ++++++++-
 .../org/apache/hop/ui/core/widget/TableView.java   | 340 +++++++++++++++++++++
 .../configuration/tabs/ConfigGuiOptionsTab.java    | 101 +++---
 .../core/dialog/messages/messages_en_US.properties |   4 +
 .../core/widget/messages/messages_en_US.properties |   1 +
 6 files changed, 583 insertions(+), 35 deletions(-)

diff --git a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java 
b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
index a40367a16f..839eb14349 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
@@ -95,6 +95,13 @@ public class PropsUi extends Props {
   private static final String USE_ADVANCED_TERMINAL = "UseAdvancedTerminal";
   private static final String RESET_DIALOG_POSITIONS_ON_RESTART = 
"ResetDialogPositionsOnRestart";
 
+  /** Max characters shown in a preview grid cell before truncation (0 = no 
truncation). */
+  private static final String PREVIEW_MAX_CELL_LENGTH = 
"Preview.MaxCellLength";
+
+  /** Render line breaks / tabs as symbols in preview grid cells instead of 
cutting at the break. */
+  private static final String PREVIEW_SHOW_LINE_BREAKS_AS_SYMBOLS =
+      "Preview.ShowLineBreaksAsSymbols";
+
   // Metrics panel (pipeline execution grid) – "Show" options
   private static final String METRICS_PANEL_SHOW_UNITS = 
"MetricsPanel.ShowUnits";
   private static final String METRICS_PANEL_SHOW_INPUT = 
"MetricsPanel.ShowInput";
@@ -605,6 +612,31 @@ public class PropsUi extends Props {
     setProperty(STRING_SHOW_TABLE_VIEW_TOOLBAR, show ? YES : NO);
   }
 
+  /**
+   * Maximum number of characters shown in a preview grid cell before it is 
truncated (double-click
+   * the cell to see the full value). 0 disables truncation. Default 50.
+   */
+  public int getMaxPreviewCellLength() {
+    return Const.toInt(getProperty(PREVIEW_MAX_CELL_LENGTH), 50);
+  }
+
+  public void setMaxPreviewCellLength(int length) {
+    setProperty(PREVIEW_MAX_CELL_LENGTH, Integer.toString(length));
+  }
+
+  /**
+   * Whether line breaks and tabs in preview grid cells are shown as 
single-line symbols (↵ / →).
+   * When false (default), a value with line breaks is instead cut at the 
first break and marked
+   * with an ellipsis.
+   */
+  public boolean isShowPreviewLineBreaksAsSymbols() {
+    return 
YES.equalsIgnoreCase(getProperty(PREVIEW_SHOW_LINE_BREAKS_AS_SYMBOLS, NO));
+  }
+
+  public void setShowPreviewLineBreaksAsSymbols(boolean show) {
+    setProperty(PREVIEW_SHOW_LINE_BREAKS_AS_SYMBOLS, show ? YES : NO);
+  }
+
   /** Show units in grid cells (default false). */
   public boolean isMetricsPanelShowUnits() {
     return YES.equalsIgnoreCase(getProperty(METRICS_PANEL_SHOW_UNITS, NO));
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 3b11c8bf86..399ec41aad 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
@@ -43,7 +43,9 @@ import org.eclipse.swt.SWT;
 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;
@@ -52,6 +54,7 @@ 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 an ArrayList of rows in a TableView. */
 public class PreviewRowsDialog {
@@ -70,6 +73,9 @@ public class PreviewRowsDialog {
 
   private Shell shell;
 
+  /** Lightweight floating box showing the full value of an overflowing cell 
(Ctrl+Space style). */
+  private Shell valueOverlay;
+
   private final List<Object[]> buffer;
 
   private String title;
@@ -289,12 +295,24 @@ public class PreviewRowsDialog {
       columns[i].setToolTip(valueMeta.toStringMeta());
       columns[i].setValueMeta(valueMeta);
       columns[i].setImage(GuiResource.getInstance().getImage(valueMeta));
+      // A preview is a viewer: don't open an inline editor on click. This 
also frees double-click
+      // to pop up the full, untruncated value of a cell.
+      columns[i].setReadOnly(true);
     }
 
     wFields =
         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.
+    wFields.setSortable(false);
+
+    // Cells only show a truncated, single-lined value for performance. 
Double-click a cell to see
+    // its full, original content (handy for long strings, JSON and multi-line 
values).
+    wFields.table.addListener(
+        SWT.MouseDoubleClick, event -> showFullCellValue(new Point(event.x, 
event.y)));
 
     FormData fdFields = new FormData();
     fdFields.left = new FormAttachment(0, 0);
@@ -397,7 +415,7 @@ public class PreviewRowsDialog {
       }
 
       if (show != null) {
-        item.setText(c + 1, show);
+        item.setText(c + 1, TableView.formatCellValueForDisplay(show));
         item.setForeground(c + 1, GuiResource.getInstance().getColorBlack());
       } else {
         // Set null value
@@ -409,6 +427,126 @@ public class PreviewRowsDialog {
     return nrErrors;
   }
 
+  /**
+   * When a cell whose value overflows the grid display is double-clicked, 
expand it in a
+   * lightweight inline multi-line box anchored to the cell. Does nothing for 
cells that aren't data
+   * cells or whose value already fits.
+   */
+  private void showFullCellValue(Point point) {
+    if (wFields == null || wFields.isDisposed() || buffer == null || rowMeta 
== null) {
+      return;
+    }
+    TableItem item = wFields.table.getItem(point);
+    if (item == null) {
+      return;
+    }
+    int rowIndex = wFields.table.indexOf(item);
+
+    // Column 0 is the row-number column; data columns start at 1. Find the 
one under the pointer.
+    int columnIndex = -1;
+    Rectangle cellBounds = null;
+    for (int i = 1; i < wFields.table.getColumnCount(); i++) {
+      Rectangle b = item.getBounds(i);
+      if (b.contains(point)) {
+        columnIndex = i;
+        cellBounds = b;
+        break;
+      }
+    }
+    if (columnIndex < 1 || cellBounds == null) {
+      return;
+    }
+    int column = columnIndex - 1;
+    if (rowIndex < 0 || rowIndex >= buffer.size() || column >= rowMeta.size()) 
{
+      return;
+    }
+
+    String full = getFullCellString(rowIndex, column);
+    if (full == null) {
+      return;
+    }
+    // Only expand cells whose value actually overflows what the grid shows.
+    int maxLength = PropsUi.getInstance().getMaxPreviewCellLength();
+    boolean overflow =
+        (maxLength > 0 && full.length() > maxLength)
+            || full.indexOf('\n') >= 0
+            || full.indexOf('\r') >= 0
+            || full.indexOf('\t') >= 0;
+    if (overflow) {
+      showValueOverlay(cellBounds, full);
+    }
+  }
+
+  /**
+   * 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);
+
+    final Shell overlay = new Shell(shell, SWT.NONE);
+    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);
+
+    overlay.setSize(Math.max(cellBounds.width, 400), 200);
+    overlay.setLocation(location.x, location.y);
+
+    // Dismiss on Escape or when focus leaves the box (mirrors the variable 
helper).
+    text.addListener(SWT.FocusOut, e -> overlay.dispose());
+    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.
+    overlay
+        .getDisplay()
+        .asyncExec(
+            () -> {
+              if (!text.isDisposed()) {
+                text.setFocus();
+                text.setSelection(0, 0);
+              }
+            });
+  }
+
+  /** 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/widget/TableView.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
index fe29824a04..b9bf484f2e 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
@@ -83,6 +83,7 @@ import org.eclipse.swt.events.TraverseListener;
 import org.eclipse.swt.graphics.Color;
 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;
@@ -92,10 +93,12 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Listener;
 import org.eclipse.swt.widgets.Menu;
 import org.eclipse.swt.widgets.MenuItem;
 import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.Table;
 import org.eclipse.swt.widgets.TableColumn;
 import org.eclipse.swt.widgets.TableItem;
@@ -181,6 +184,13 @@ public class TableView extends Composite {
   @Getter private TableItem activeTableItem;
   @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 currently open multi-line pop-out editor, if any (guards against 
stacking two). */
+  private Shell multilineShell;
+
   private final KeyListener lsKeyText;
   private final KeyListener lsKeyCombo;
   private final FocusListener lsFocusText;
@@ -517,6 +527,21 @@ public class TableView extends Composite {
     fdTable.bottom = new FormAttachment(100, 0);
     table.setLayoutData(fdTable);
 
+    // Hop Web: RWT can't render line breaks in a table cell and can't 
owner-draw over it (both of
+    // which we use on the desktop). Add a footnote pointing users to the 
editor for the full value.
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      Label webNewlineHint = new Label(this, SWT.LEFT);
+      PropsUi.setLook(webNewlineHint);
+      webNewlineHint.setText(BaseMessages.getString(PKG, 
"TableView.WebNewlineHint.Label"));
+      FormData fdHint = new FormData();
+      fdHint.left = new FormAttachment(0, 0);
+      fdHint.right = new FormAttachment(100, 0);
+      fdHint.bottom = new FormAttachment(100, 0);
+      webNewlineHint.setLayoutData(fdHint);
+      // The table now stops just above the footnote.
+      fdTable.bottom = new FormAttachment(webNewlineHint, 
-PropsUi.getMargin());
+    }
+
     tableColumn = new TableColumn[columns.length + 1];
     tableColumn[0] = new TableColumn(table, SWT.RIGHT);
     tableColumn[0].setResizable(true);
@@ -598,6 +623,18 @@ 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());
+
+    // 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
+    // hop-web these are inert and the cell falls back to RWT's native 
rendering (which collapses
+    // line breaks); the value itself stays intact and is shown in full by the 
multi-line editor.
+    if (!EnvironmentUtils.getInstance().isWeb()) {
+      table.addListener(SWT.EraseItem, this::eraseCell);
+      table.addListener(SWT.PaintItem, this::paintCell);
+    }
+
     // Add support for sorted columns!
     //
     final int nrcols = tableColumn.length;
@@ -1902,6 +1939,271 @@ 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.
+   */
+  private void onCellDoubleClick() {
+    if (readonly || !table.isEnabled() || !isEnabled() || columns.length == 0) 
{
+      return;
+    }
+    if (activeTableItem == null || activeTableItem.isDisposed()) {
+      return;
+    }
+    int rowNr = activeTableRow;
+    int colNr = activeTableColumn;
+    if (colNr < 1 || colNr - 1 >= columns.length || rowNr < 0 || rowNr >= 
table.getItemCount()) {
+      return;
+    }
+    ColumnInfo colinfo = columns[colNr - 1];
+    if (colinfo == null
+        || (colinfo.getType() != ColumnInfo.COLUMN_TYPE_TEXT
+            && colinfo.getType() != ColumnInfo.COLUMN_TYPE_TEXT_BUTTON)
+        || colinfo.isReadOnly()) {
+      return;
+    }
+    if (colinfo.getDisabledListener() != null
+        && colinfo.getDisabledListener().isFieldDisabled(rowNr)) {
+      return;
+    }
+    editMultiline(activeTableItem, rowNr, colNr, colinfo);
+  }
+
+  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.
+    if (multilineShell != null && !multilineShell.isDisposed()) {
+      multilineShell.setFocus();
+      return;
+    }
+    // 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;
+    if (text != null && !text.isDisposed()) {
+      seed = getTextWidgetValue(colNr);
+      text.dispose();
+    } else {
+      seed = row.getText(colNr);
+    }
+
+    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));
+
+    Rectangle cellBounds = row.getBounds(colNr);
+    Point location = table.toDisplay(cellBounds.x, cellBounds.y);
+
+    final Shell popup = new Shell(getShell(), SWT.NONE);
+    multilineShell = popup;
+    popup.addListener(
+        SWT.Dispose,
+        e -> {
+          if (multilineShell == popup) {
+            multilineShell = null;
+          }
+        });
+    popup.setLayout(new FillLayout());
+
+    final Text multi = new Text(popup, SWT.MULTI | SWT.WRAP | SWT.V_SCROLL | 
SWT.BORDER);
+    PropsUi.setLook(multi);
+    // 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));
+    }
+
+    popup.setSize(Math.max(cellBounds.width, 400), 200);
+    popup.setLocation(location.x, location.y);
+
+    final boolean[] closed = {false};
+    Runnable commit =
+        () -> {
+          if (closed[0] || multi.isDisposed() || row.isDisposed()) {
+            return;
+          }
+          closed[0] = true;
+          // Store platform-independent \n line breaks (matches how XML 
round-trips the value).
+          String newValue = toUnixLineBreaks(multi.getText());
+          popup.dispose();
+          if (!table.isDisposed()) {
+            table.setFocus();
+          }
+          if (!newValue.equals(row.getText(colNr))) {
+            row.setText(colNr, newValue);
+          }
+          String[] afterEdit = getItemText(row);
+          checkChanged(new String[][] {beforeEdit}, new String[][] 
{afterEdit}, new int[] {rowNr});
+          fireContentChangedListener(rowNr, colNr, newValue);
+        };
+    Runnable cancel =
+        () -> {
+          if (closed[0]) {
+            return;
+          }
+          closed[0] = true;
+          fieldChanged = false;
+          popup.dispose();
+          if (!table.isDisposed()) {
+            table.setFocus();
+          }
+        };
+
+    // Commit when focus leaves the box; Ctrl+Enter also commits; Escape 
cancels.
+    multi.addListener(SWT.FocusOut, e -> commit.run());
+    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) {
+            commit.run();
+          }
+        });
+
+    popup.open();
+    // Grab focus after the double-click settles so a trailing focus event 
can't self-close it.
+    getDisplay()
+        .asyncExec(
+            () -> {
+              if (!multi.isDisposed()) {
+                multi.setFocus();
+                multi.setSelection(multi.getText().length());
+              }
+            });
+  }
+
+  /**
+   * Format a cell value for display in a grid: keep it single-line and short 
so the native table
+   * stays fast. Honors the Look &amp; Feel settings {@link 
PropsUi#getMaxPreviewCellLength()} and
+   * {@link PropsUi#isShowPreviewLineBreaksAsSymbols()}. Returns null for a 
null value.
+   */
+  public static String formatCellValueForDisplay(String value) {
+    if (value == null) {
+      return null;
+    }
+    PropsUi props = PropsUi.getInstance();
+    int maxLength = props.getMaxPreviewCellLength();
+    boolean lineBreaksAsSymbols = props.isShowPreviewLineBreaksAsSymbols();
+
+    String display = value;
+    boolean truncated = false;
+
+    // Cap the length first so we never scan a huge value further than needed.
+    if (maxLength > 0 && display.length() > maxLength) {
+      display = display.substring(0, maxLength);
+      truncated = true;
+    }
+    // Unless line breaks are shown as symbols, cut at the first break to stay 
on one line.
+    if (!lineBreaksAsSymbols) {
+      int breakIndex = indexOfLineBreak(display);
+      if (breakIndex >= 0) {
+        display = display.substring(0, breakIndex);
+        truncated = true;
+      }
+    }
+    if (truncated) {
+      display = display + " …";
+    }
+    // Render remaining line breaks / tabs as single-line symbols when the 
option is enabled.
+    if (lineBreaksAsSymbols
+        && (display.indexOf('\n') >= 0
+            || display.indexOf('\r') >= 0
+            || display.indexOf('\t') >= 0)) {
+      display =
+          display
+              .replace("\r\n", " ↵ ")
+              .replace("\n", " ↵ ")
+              .replace("\r", " ↵ ")
+              .replace("\t", " → ");
+    }
+    return display;
+  }
+
+  /** Index of the first line-break character (CR or LF), or -1 if there is 
none. */
+  private static int indexOfLineBreak(String s) {
+    for (int i = 0; i < s.length(); i++) {
+      char c = s.charAt(i);
+      if (c == '\n' || c == '\r') {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /** Normalize any line breaks to a single '\n' — platform-independent, and 
how XML round-trips. */
+  private static String toUnixLineBreaks(String s) {
+    return s == null ? null : s.replace("\r\n", "\n").replace('\r', '\n');
+  }
+
+  /**
+   * Convert line breaks to the platform delimiter so a multi-line {@link 
Text} actually renders
+   * them. On Windows a multi-line Text needs "\r\n"; a lone "\n" shows as a 
control-char box.
+   */
+  private static String toPlatformLineBreaks(String s) {
+    if (s == null) {
+      return null;
+    }
+    String unix = toUnixLineBreaks(s);
+    return "\n".equals(Text.DELIMITER) ? unix : unix.replace("\n", 
Text.DELIMITER);
+  }
+
+  /**
+   * The shortened display string for a text cell whose stored value is longer 
/ multi-line, or null
+   * when the cell should be drawn natively (non-text column, or nothing to 
shorten). Used by the
+   * desktop owner-draw so {@link TableItem#getText(int)} keeps returning the 
full, saved value.
+   */
+  private String customCellText(TableItem item, int columnIndex) {
+    if (item == null || columnIndex < 1 || columnIndex - 1 >= columns.length) {
+      return null;
+    }
+    ColumnInfo colinfo = columns[columnIndex - 1];
+    if (colinfo == null
+        || (colinfo.getType() != ColumnInfo.COLUMN_TYPE_TEXT
+            && colinfo.getType() != ColumnInfo.COLUMN_TYPE_TEXT_BUTTON)) {
+      return null;
+    }
+    String full = item.getText(columnIndex);
+    String display = formatCellValueForDisplay(full);
+    return display != null && !display.equals(full) ? display : null;
+  }
+
+  private void eraseCell(Event event) {
+    // Suppress native text drawing for cells we redraw ourselves; keep 
background & selection.
+    if (customCellText((TableItem) event.item, event.index) != null) {
+      event.detail &= ~SWT.FOREGROUND;
+    }
+  }
+
+  private void paintCell(Event event) {
+    String display = customCellText((TableItem) event.item, event.index);
+    if (display == null) {
+      return;
+    }
+    TableItem item = (TableItem) event.item;
+    Color foreground;
+    if ((event.detail & SWT.SELECTED) != 0) {
+      foreground = getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT);
+    } else {
+      foreground = item.getForeground(event.index);
+    }
+    event.gc.setForeground(foreground);
+    Point size = event.gc.textExtent(display);
+    int yOffset = Math.max(0, (event.height - size.y) / 2);
+    event.gc.drawText(display, event.x + 2, event.y + yOffset, 
SWT.DRAW_TRANSPARENT);
+  }
+
   private void checkChanged(String[][] before, String[][] after, int[] index) {
     // Did we change anything: if so, add undo information
     if (fieldChanged) {
@@ -2595,6 +2897,15 @@ public class TableView extends Composite {
       return;
     }
 
+    // A single-line editor can't represent a multi-line value (control-char 
boxes on Windows), so
+    // edit values that contain a line break in the multi-line pop-out editor 
instead.
+    if ((colinfo.getType() == ColumnInfo.COLUMN_TYPE_TEXT
+            || colinfo.getType() == ColumnInfo.COLUMN_TYPE_TEXT_BUTTON)
+        && indexOfLineBreak(row.getText(colNr)) >= 0) {
+      editMultiline(row, rowNr, colNr, colinfo);
+      return;
+    }
+
     String content = row.getText(colNr) + (extra != 0 ? "" + extra : "");
     String tooltip = columns[colNr - 1].getToolTip();
 
@@ -2688,6 +2999,35 @@ 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();
+            }
+          });
+    }
+    inlineEditorOpenedAt = System.currentTimeMillis();
+
     int width = tableColumn[colNr].getWidth();
     int height = 30;
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
index 3416959a22..b59578e001 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
@@ -104,6 +104,8 @@ public class ConfigGuiOptionsTab {
   private Button wMetricsOnTransforms;
   private Button wHideMenuBar;
   private Button wShowTableViewToolbar;
+  private Text wMaxPreviewCellLength;
+  private Button wShowPreviewLineBreaks;
   private Button wMetricsPanelShowUnits;
   private Button wMetricsPanelShowInput;
   private Button wMetricsPanelShowRead;
@@ -206,6 +208,12 @@ public class ConfigGuiOptionsTab {
       wEnableInfiniteMove.setSelection(props.isInfiniteCanvasMoveEnabled());
       wHideMenuBar.setSelection(props.isHidingMenuBar());
       wShowTableViewToolbar.setSelection(props.isShowTableViewToolbar());
+      if (wMaxPreviewCellLength != null && 
!wMaxPreviewCellLength.isDisposed()) {
+        
wMaxPreviewCellLength.setText(Integer.toString(props.getMaxPreviewCellLength()));
+      }
+      if (wShowPreviewLineBreaks != null && 
!wShowPreviewLineBreaks.isDisposed()) {
+        
wShowPreviewLineBreaks.setSelection(props.isShowPreviewLineBreaksAsSymbols());
+      }
       if (wMetricsPanelShowUnits != null && 
!wMetricsPanelShowUnits.isDisposed()) {
         wMetricsPanelShowUnits.setSelection(props.isMetricsPanelShowUnits());
         wMetricsPanelShowInput.setSelection(props.isMetricsPanelShowInput());
@@ -317,7 +325,7 @@ public class ConfigGuiOptionsTab {
     noteFont = new Font(shell.getDisplay(), noteFontData);
 
     // Track the last control for vertical positioning
-    org.eclipse.swt.widgets.Control lastControl = null;
+    Control lastControl = null;
 
     // Expand all / Collapse all buttons for the sections below
     Composite wExpandButtons = new Composite(wLookComp, SWT.NONE);
@@ -348,7 +356,7 @@ public class ConfigGuiOptionsTab {
     lastControl = wExpandButtons;
 
     // Preferred language - at the top
-    org.eclipse.swt.widgets.Control[] defaultLocaleControls =
+    Control[] defaultLocaleControls =
         createComboField(
             wLookComp,
             "EnterOptionsDialog.DefaultLocale.Label",
@@ -427,10 +435,10 @@ public class ConfigGuiOptionsTab {
     appearanceContent.setLayout(appearanceLayout);
 
     // Appearance controls inside the expandable content
-    org.eclipse.swt.widgets.Control lastAppearanceControl = null;
+    Control lastAppearanceControl = null;
 
     // Global zoom (at the top)
-    org.eclipse.swt.widgets.Control[] globalZoomControls =
+    Control[] globalZoomControls =
         createComboField(
             appearanceContent,
             "EnterOptionsDialog.GlobalZoom.Label",
@@ -444,7 +452,7 @@ public class ConfigGuiOptionsTab {
     lastAppearanceControl = wGlobalZoom;
 
     // Icon size
-    org.eclipse.swt.widgets.Control[] iconSizeControls =
+    Control[] iconSizeControls =
         createTextField(
             appearanceContent,
             "EnterOptionsDialog.IconSize.Label",
@@ -467,7 +475,7 @@ public class ConfigGuiOptionsTab {
     lastAppearanceControl = wIconSize;
 
     // Line width
-    org.eclipse.swt.widgets.Control[] lineWidthControls =
+    Control[] lineWidthControls =
         createTextField(
             appearanceContent,
             "EnterOptionsDialog.LineWidth.Label",
@@ -490,7 +498,7 @@ public class ConfigGuiOptionsTab {
     lastAppearanceControl = wLineWidth;
 
     // Dialog middle percentage
-    org.eclipse.swt.widgets.Control[] middlePctControls =
+    Control[] middlePctControls =
         createTextField(
             appearanceContent,
             "EnterOptionsDialog.DialogMiddlePercentage.Label",
@@ -564,10 +572,10 @@ public class ConfigGuiOptionsTab {
     fontsContent.setLayout(fontsLayout);
 
     // Fonts inside the expandable content
-    org.eclipse.swt.widgets.Control lastFontControl = null;
+    Control lastFontControl = null;
 
     // Default font
-    org.eclipse.swt.widgets.Control[] defaultFontControls =
+    Control[] defaultFontControls =
         createFontPicker(
             fontsContent, "EnterOptionsDialog.DefaultFont.Label", shell, 
lastFontControl, margin);
     wDefaultCanvas = (Canvas) defaultFontControls[0];
@@ -580,7 +588,7 @@ public class ConfigGuiOptionsTab {
     lastFontControl = wDefaultCanvas;
 
     // Fixed width font
-    org.eclipse.swt.widgets.Control[] fixedFontControls =
+    Control[] fixedFontControls =
         createFontPicker(
             fontsContent,
             "EnterOptionsDialog.FixedWidthFont.Label",
@@ -597,7 +605,7 @@ public class ConfigGuiOptionsTab {
     lastFontControl = wFixedCanvas;
 
     // Graph font
-    org.eclipse.swt.widgets.Control[] graphFontControls =
+    Control[] graphFontControls =
         createFontPicker(
             fontsContent, "EnterOptionsDialog.GraphFont.Label", shell, 
lastFontControl, margin);
     wGraphCanvas = (Canvas) graphFontControls[0];
@@ -610,7 +618,7 @@ public class ConfigGuiOptionsTab {
     lastFontControl = wGraphCanvas;
 
     // Note font
-    org.eclipse.swt.widgets.Control[] noteFontControls =
+    Control[] noteFontControls =
         createFontPicker(
             fontsContent, "EnterOptionsDialog.NoteFont.Label", shell, 
lastFontControl, margin);
     wNoteCanvas = (Canvas) noteFontControls[0];
@@ -673,7 +681,7 @@ public class ConfigGuiOptionsTab {
     canvasContent.setLayout(canvasLayout);
 
     // Show canvas grid checkbox inside the expandable content
-    org.eclipse.swt.widgets.Control lastCanvasControl = null;
+    Control lastCanvasControl = null;
     wShowCanvasGrid =
         createCheckbox(
             canvasContent,
@@ -685,7 +693,7 @@ public class ConfigGuiOptionsTab {
     lastCanvasControl = wShowCanvasGrid;
 
     // Grid size - placed under Show canvas grid checkbox
-    org.eclipse.swt.widgets.Control[] gridSizeControls =
+    Control[] gridSizeControls =
         createTextField(
             canvasContent,
             "EnterOptionsDialog.GridSize.Label",
@@ -825,7 +833,7 @@ public class ConfigGuiOptionsTab {
     autoLayoutLayout.marginHeight = PropsUi.getFormMargin();
     autoLayoutContent.setLayout(autoLayoutLayout);
 
-    org.eclipse.swt.widgets.Control lastAutoLayoutControl = null;
+    Control lastAutoLayoutControl = null;
 
     // Direction
     String[] directionLabels = {
@@ -834,7 +842,7 @@ public class ConfigGuiOptionsTab {
       BaseMessages.getString(PKG, 
"EnterOptionsDialog.AutoLayout.Direction.TopBottom"),
       BaseMessages.getString(PKG, 
"EnterOptionsDialog.AutoLayout.Direction.BottomTop")
     };
-    org.eclipse.swt.widgets.Control[] directionControls =
+    Control[] directionControls =
         createComboField(
             autoLayoutContent,
             "EnterOptionsDialog.AutoLayout.Direction.Label",
@@ -849,7 +857,7 @@ public class ConfigGuiOptionsTab {
     lastAutoLayoutControl = wAutoLayoutDirection;
 
     // Layer spacing
-    org.eclipse.swt.widgets.Control[] layerSpacingControls =
+    Control[] layerSpacingControls =
         createTextField(
             autoLayoutContent,
             "EnterOptionsDialog.AutoLayout.LayerSpacing.Label",
@@ -864,7 +872,7 @@ public class ConfigGuiOptionsTab {
     lastAutoLayoutControl = wAutoLayoutLayerSpacing;
 
     // Node spacing
-    org.eclipse.swt.widgets.Control[] nodeSpacingControls =
+    Control[] nodeSpacingControls =
         createTextField(
             autoLayoutContent,
             "EnterOptionsDialog.AutoLayout.NodeSpacing.Label",
@@ -879,7 +887,7 @@ public class ConfigGuiOptionsTab {
     lastAutoLayoutControl = wAutoLayoutNodeSpacing;
 
     // Crossing-reduction iterations
-    org.eclipse.swt.widgets.Control[] iterationsControls =
+    Control[] iterationsControls =
         createTextField(
             autoLayoutContent,
             "EnterOptionsDialog.AutoLayout.CrossingIterations.Label",
@@ -953,7 +961,7 @@ public class ConfigGuiOptionsTab {
     tablesContent.setLayout(tablesLayout);
 
     // Show toolbar checkbox inside the expandable content
-    org.eclipse.swt.widgets.Control lastTablesControl = null;
+    Control lastTablesControl = null;
     wShowTableViewToolbar =
         createCheckbox(
             tablesContent,
@@ -962,6 +970,32 @@ public class ConfigGuiOptionsTab {
             props.isShowTableViewToolbar(),
             lastTablesControl,
             margin);
+    lastTablesControl = wShowTableViewToolbar;
+
+    // Maximum number of characters shown in a preview grid cell before it is 
truncated.
+    Control[] maxPreviewCellLengthControls =
+        createTextField(
+            tablesContent,
+            "EnterOptionsDialog.MaxPreviewCellLength.Label",
+            "EnterOptionsDialog.MaxPreviewCellLength.ToolTip",
+            Integer.toString(props.getMaxPreviewCellLength()),
+            lastTablesControl,
+            margin);
+    wMaxPreviewCellLength = (Text) maxPreviewCellLengthControls[1];
+    wMaxPreviewCellLength.setMessage(
+        BaseMessages.getString(PKG, ENTER_OPTIONS_DIALOG_ENTER_NUMBER_HINT));
+    wMaxPreviewCellLength.addListener(SWT.Verify, this::verifyNumber);
+    lastTablesControl = wMaxPreviewCellLength;
+
+    // Show line breaks / tabs as symbols in preview cells (default off: cut 
at the first break).
+    wShowPreviewLineBreaks =
+        createCheckbox(
+            tablesContent,
+            "EnterOptionsDialog.ShowPreviewLineBreaks.Label",
+            "EnterOptionsDialog.ShowPreviewLineBreaks.ToolTip",
+            props.isShowPreviewLineBreaksAsSymbols(),
+            lastTablesControl,
+            margin);
 
     // Create the expand item
     ExpandItem tablesItem = new ExpandItem(tablesExpandBar, SWT.NONE);
@@ -1395,6 +1429,9 @@ public class ConfigGuiOptionsTab {
     props.setDarkMode(darkMode);
     props.setHidingMenuBar(wHideMenuBar.getSelection());
     props.setShowTableViewToolbar(wShowTableViewToolbar.getSelection());
+    props.setMaxPreviewCellLength(
+        Const.toInt(wMaxPreviewCellLength.getText(), 
props.getMaxPreviewCellLength()));
+    
props.setShowPreviewLineBreaksAsSymbols(wShowPreviewLineBreaks.getSelection());
     props.setMetricsPanelShowUnits(wMetricsPanelShowUnits.getSelection());
     props.setMetricsPanelShowInput(wMetricsPanelShowInput.getSelection());
     props.setMetricsPanelShowRead(wMetricsPanelShowRead.getSelection());
@@ -1509,12 +1546,12 @@ public class ConfigGuiOptionsTab {
     }
   }
 
-  private org.eclipse.swt.widgets.Control[] createTextField(
+  private Control[] createTextField(
       Composite parent,
       String labelKey,
       String tooltipKey,
       String initialValue,
-      org.eclipse.swt.widgets.Control lastControl,
+      Control lastControl,
       int margin) {
     // Label above
     Label label = new Label(parent, SWT.LEFT);
@@ -1546,7 +1583,7 @@ public class ConfigGuiOptionsTab {
     fdText.top = new FormAttachment(label, margin / 2);
     text.setLayoutData(fdText);
 
-    return new org.eclipse.swt.widgets.Control[] {label, text};
+    return new Control[] {label, text};
   }
 
   /**
@@ -1560,12 +1597,12 @@ public class ConfigGuiOptionsTab {
    * @param margin The margin to use
    * @return An array containing [Label, Combo] controls
    */
-  private org.eclipse.swt.widgets.Control[] createComboField(
+  private Control[] createComboField(
       Composite parent,
       String labelKey,
       String tooltipKey,
       String[] items,
-      org.eclipse.swt.widgets.Control lastControl,
+      Control lastControl,
       int margin) {
     // Label above
     Label label = new Label(parent, SWT.LEFT);
@@ -1597,7 +1634,7 @@ public class ConfigGuiOptionsTab {
     fdCombo.top = new FormAttachment(label, margin / 2);
     combo.setLayoutData(fdCombo);
 
-    return new org.eclipse.swt.widgets.Control[] {label, combo};
+    return new Control[] {label, combo};
   }
 
   /**
@@ -1616,7 +1653,7 @@ public class ConfigGuiOptionsTab {
       String labelKey,
       String tooltipKey,
       boolean selected,
-      org.eclipse.swt.widgets.Control lastControl,
+      Control lastControl,
       int margin) {
     Button checkbox = new Button(parent, SWT.CHECK);
     PropsUi.setLook(checkbox);
@@ -1650,12 +1687,8 @@ public class ConfigGuiOptionsTab {
    * @param margin The margin to use
    * @return An array containing [Canvas, EditButton, ResetButton] controls
    */
-  private org.eclipse.swt.widgets.Control[] createFontPicker(
-      Composite parent,
-      String labelKey,
-      Shell shell,
-      org.eclipse.swt.widgets.Control lastControl,
-      int margin) {
+  private Control[] createFontPicker(
+      Composite parent, String labelKey, Shell shell, Control lastControl, int 
margin) {
     int h = (int) (40 * PropsUi.getInstance().getZoomFactor());
 
     // Label above
@@ -1701,6 +1734,6 @@ public class ConfigGuiOptionsTab {
     fdCanvas.height = h;
     canvas.setLayoutData(fdCanvas);
 
-    return new org.eclipse.swt.widgets.Control[] {canvas, editButton, 
resetButton};
+    return new Control[] {canvas, editButton, resetButton};
   }
 }
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 9568d8e2f0..b6d4cbe6eb 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
@@ -188,6 +188,10 @@ EnterOptionsDialog.ShowCanvasGrid.Label=Show canvas grid
 EnterOptionsDialog.ShowCanvasGrid.ToolTip=If enabled, the canvas grid will be 
visible
 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.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.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 a754049e5b..8beca3c5a4 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
@@ -81,6 +81,7 @@ 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.ToolBarWidget.MoveRowsDown.ToolTip=Move selected rows down
 TableView.ToolBarWidget.MoveRowsUp.ToolTip=Move selected rows up
 TableView.ToolBarWidget.PasteSelected.ToolTip=Paste clipboard to the table 
after the selected row


Reply via email to