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 & 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