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