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 0dd7ad630e Fix #8360: read-only grid selects the row on click and
keeps its context menu (#8530)
0dd7ad630e is described below
commit 0dd7ad630e464e336bea3cbe89d50dc8adf9a5ff
Author: Frank Skorgen <[email protected]>
AuthorDate: Wed Oct 14 16:08:59 2026 +0200
Fix #8360: read-only grid selects the row on click and keeps its context
menu (#8530)
* Fix #8360: read-only grid selects the row on click, double-click opens
the cell
* rebase and fix remarks
* fix remarks add tests
---------
Co-authored-by: Hans Van Akelyen <[email protected]>
---
.../ui/core/widget/TableViewReadOnlyGridTest.java | 408 +++++++++++++++++++++
.../org/apache/hop/ui/core/widget/TableView.java | 188 ++++++++--
.../hop/ui/pipeline/debug/PipelineDebugDialog.java | 26 +-
.../core/widget/messages/messages_en_US.properties | 1 +
4 files changed, 574 insertions(+), 49 deletions(-)
diff --git
a/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewReadOnlyGridTest.java
b/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewReadOnlyGridTest.java
new file mode 100644
index 0000000000..631cf1d468
--- /dev/null
+++
b/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewReadOnlyGridTest.java
@@ -0,0 +1,408 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.core.widget;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import org.apache.hop.core.gui.plugin.GuiRegistry;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolItem;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * A read-only grid ({@link TableView#setReadonly(boolean)}): a click selects
the row, a
+ * double-click opens the cell as a viewer that never writes back, and the
selection and copy
+ * actions work.
+ *
+ * <p>Clicks and menu opens are sent straight to the table's listeners. That
is also what proves the
+ * grid does its own bookkeeping, such as refreshing the toolbar, without
counting on a native
+ * {@link SWT#Selection} that platforms do not reliably send after the row was
selected in code.
+ */
+@Tag("uitest")
+class TableViewReadOnlyGridTest extends SwtBotTestBase {
+
+ /**
+ * The grid's toolbar items are its own {@code @GuiToolbarElement} methods,
which the Hop GUI
+ * registers at startup. Register them the same way here so the grid gets
its toolbar.
+ */
+ @BeforeAll
+ static void registerTableViewToolbar() {
+ GuiRegistry registry = GuiRegistry.getInstance();
+ for (Method method : TableView.class.getDeclaredMethods()) {
+ GuiToolbarElement element =
method.getAnnotation(GuiToolbarElement.class);
+ if (element != null && registry.findGuiToolbarItem(element.root(),
element.id()) == null) {
+ registry.addGuiToolbarElement(
+ TableView.class.getName(), element, method,
TableView.class.getClassLoader());
+ }
+ }
+ }
+
+ @Test
+ void clickSelectsTheRowAndEnablesCopyWithoutOpeningAnEditor() {
+ AtomicReference<TableView> viewRef = new AtomicReference<>();
+ withScene(
+ shell -> viewRef.set(readOnlyGrid(shell, plainColumns(), null)),
+ bot -> {
+ waitForLayout(viewRef.get());
+ Object[] state =
+ onUi(
+ () -> {
+ TableView view = viewRef.get();
+ ToolItem copy =
+
view.getToolbarWidgets().findToolItem(TableView.ID_TOOLBAR_COPY_SELECTED);
+ assertNotNull(copy, "the Copy toolbar item");
+ boolean copyEnabledBefore = copy.isEnabled();
+ sendMouse(view, SWT.MouseDown, 1, 1);
+ return new Object[] {
+ copyEnabledBefore,
+ view.table.getSelectionIndex(),
+ copy.isEnabled(),
+ findText(view.table)
+ };
+ });
+ assertFalse((Boolean) state[0], "Copy starts disabled");
+ assertEquals(1, state[1]);
+ assertTrue((Boolean) state[2], "Copy is enabled once the click
selected a row");
+ assertNull(state[3], "a single click opens no editor");
+ });
+ }
+
+ @Test
+ void doubleClickOpensAViewerThatNeverWritesBack() {
+ AtomicReference<TableView> viewRef = new AtomicReference<>();
+ AtomicInteger modifications = new AtomicInteger();
+ withScene(
+ shell ->
+ viewRef.set(readOnlyGrid(shell, plainColumns(), e ->
modifications.incrementAndGet())),
+ bot -> {
+ waitForLayout(viewRef.get());
+ Object[] state =
+ onUi(
+ () -> {
+ TableView view = viewRef.get();
+ doubleClick(view, 0, 2);
+ Text viewer = findText(view.table);
+ assertNotNull(viewer, "a double-click opens the cell");
+ boolean readOnlyStyle = (viewer.getStyle() &
SWT.READ_ONLY) != 0;
+ String shown = viewer.getText();
+ // Even text put into the viewer in code must not reach
the cell.
+ viewer.setText("changed");
+ // Clicking another cell closes the viewer the way any
open editor is closed.
+ sendMouse(view, SWT.MouseDown, 1, 1);
+ return new Object[] {
+ readOnlyStyle, shown, view.table.getItem(0).getText(2),
findText(view.table)
+ };
+ });
+ assertTrue((Boolean) state[0], "the viewer can't be typed in");
+ assertEquals("b0", state[1]);
+ assertEquals("b0", state[2], "the cell keeps its value");
+ assertNull(state[3], "the viewer is gone");
+ assertEquals(0, modifications.get(), "nothing reported a change");
+ });
+ }
+
+ @Test
+ void doubleClickIsLeftToAnOwnerThatOpensTheRow() {
+ AtomicReference<TableView> viewRef = new AtomicReference<>();
+ withScene(
+ shell -> {
+ TableView view = readOnlyGrid(shell, plainColumns(), null);
+ // An owner that picks or opens the double-clicked row, like the
pipeline grid does.
+ view.table.addListener(SWT.DefaultSelection, e -> {});
+ viewRef.set(view);
+ },
+ bot -> {
+ waitForLayout(viewRef.get());
+ Text viewer =
+ onUi(
+ () -> {
+ TableView view = viewRef.get();
+ doubleClick(view, 0, 1);
+ return findText(view.table);
+ });
+ assertNull(viewer, "the owner picks or opens the row, the grid opens
no cell");
+ });
+ }
+
+ @Test
+ void selectionAdapterColumnOpensTheViewerInsteadOfTheAdapter() {
+ AtomicReference<TableView> viewRef = new AtomicReference<>();
+ AtomicInteger adapterCalls = new AtomicInteger();
+ String formula = "IF([amount] > 1000; \"large\"; IF([amount] > 100;
\"medium\"; \"small\"))";
+ withScene(
+ shell -> {
+ ColumnInfo[] columns = plainColumns();
+ columns[1].setSelectionAdapter(
+ new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ adapterCalls.incrementAndGet();
+ }
+ });
+ TableView view = readOnlyGrid(shell, columns, null);
+ view.table.getItem(0).setText(2, formula);
+ viewRef.set(view);
+ },
+ bot -> {
+ waitForLayout(viewRef.get());
+ Object[] state =
+ onUi(
+ () -> {
+ TableView view = viewRef.get();
+ doubleClick(view, 0, 2);
+ Text viewer = findText(view.table);
+ return new Object[] {
+ viewer == null ? null : viewer.getText(),
+ viewer != null && (viewer.getStyle() & SWT.READ_ONLY) != 0
+ };
+ });
+ assertEquals(0, adapterCalls.get(), "the adapter edits the cell, so
it is not run");
+ assertEquals(formula, state[0], "the full value is shown instead");
+ assertTrue((Boolean) state[1]);
+ });
+ }
+
+ @Test
+ void copyCellValueCopiesTheStoredValueAndNothingForANull() {
+ AtomicReference<TableView> viewRef = new AtomicReference<>();
+ String multiLine = "first line\nsecond line";
+ withScene(
+ shell -> {
+ TableView view = readOnlyGrid(shell, plainColumns(), null);
+ view.setShortenDisplayedValues(true);
+ TableItem row = view.table.getItem(0);
+ view.setCellValue(row, 1, multiLine);
+ // A NULL, drawn the way the data grids draw it, and a value that
only reads "<null>".
+ row.setText(2, "<null>");
+ row.setForeground(2, GuiResource.getInstance().getColorBlue());
+ view.table.getItem(1).setText(2, "<null>");
+ viewRef.set(view);
+ },
+ bot -> {
+ waitForLayout(viewRef.get());
+ String[] copied =
+ onUi(
+ () -> {
+ TableView view = viewRef.get();
+ String displayed = view.table.getItem(0).getText(1);
+ sendMouse(view, SWT.MouseDown, 0, 1);
+ String stored = view.getActiveCellValueToCopy();
+ sendMouse(view, SWT.MouseDown, 0, 2);
+ String nullValue = view.getActiveCellValueToCopy();
+ sendMouse(view, SWT.MouseDown, 1, 2);
+ String text = view.getActiveCellValueToCopy();
+ return new String[] {displayed, stored, nullValue, text};
+ });
+ assertFalse(multiLine.equals(copied[0]), "the cell shows a shortened
value");
+ assertEquals(multiLine, copied[1], "the stored value is copied, not
the display text");
+ assertEquals("", copied[2], "a NULL is copied as nothing");
+ assertEquals("<null>", copied[3], "text that only reads <null> is
copied as it is");
+ });
+ }
+
+ @Test
+ void copyCellValueOnlyActsOnTheCellTheMenuWasOpenedOn() {
+ AtomicReference<TableView> viewRef = new AtomicReference<>();
+ withScene(
+ shell -> viewRef.set(readOnlyGrid(shell, plainColumns(), null)),
+ bot -> {
+ waitForLayout(viewRef.get());
+ boolean[] enabled =
+ onUi(
+ () -> {
+ TableView view = viewRef.get();
+ sendMouse(view, SWT.MouseDown, 0, 1);
+ Point onCell = center(view.table.getItem(0).getBounds(1));
+ Rectangle lastRow = view.table.getItem(1).getBounds(1);
+ Point belowRows = new Point(onCell.x, lastRow.y +
lastRow.height * 3);
+ Point otherCell =
center(view.table.getItem(1).getBounds(2));
+ return new boolean[] {
+ copyCellEnabled(view, SWT.MENU_MOUSE, onCell),
+ copyCellEnabled(view, SWT.MENU_MOUSE, belowRows),
+ // Shift/ctrl right-click does not move the active cell.
+ copyCellEnabled(view, SWT.MENU_MOUSE, otherCell),
+ copyCellEnabled(view, SWT.MENU_KEYBOARD, new Point(-1,
-1))
+ };
+ });
+ assertTrue(enabled[0], "menu opened on the active cell");
+ assertFalse(enabled[1], "menu opened below the rows");
+ assertFalse(enabled[2], "menu opened on another cell than the active
one");
+ assertTrue(enabled[3], "menu opened from the keyboard acts on the
active cell");
+ });
+ }
+
+ /** Open the row context menu the way SWT does and report whether Copy cell
value is enabled. */
+ private static boolean copyCellEnabled(TableView view, int detail, Point
tablePoint) {
+ Event detect = new Event();
+ Point displayPoint = view.table.toDisplay(tablePoint);
+ detect.x = displayPoint.x;
+ detect.y = displayPoint.y;
+ detect.detail = detail;
+ view.table.notifyListeners(SWT.MenuDetect, detect);
+ Menu menu = view.table.getMenu();
+ menu.notifyListeners(SWT.Show, new Event());
+ String label = BaseMessages.getString(TableView.class,
"TableView.menu.CopyCellValue");
+ for (MenuItem item : menu.getItems()) {
+ if (label.equals(item.getText())) {
+ return item.isEnabled();
+ }
+ }
+ throw new AssertionError("no '" + label + "' item in the row menu");
+ }
+
+ private static ColumnInfo[] plainColumns() {
+ return new ColumnInfo[] {
+ new ColumnInfo("Name", ColumnInfo.COLUMN_TYPE_TEXT, false, false),
+ new ColumnInfo("Value", ColumnInfo.COLUMN_TYPE_TEXT, false, false),
+ };
+ }
+
+ /** Plain, editable columns in a grid that is read-only as a whole, as
BaseDialog makes it. */
+ private static TableView readOnlyGrid(
+ Composite parent, ColumnInfo[] columns, ModifyListener modifyListener) {
+ parent.setLayout(new FillLayout());
+ // Room for the toolbar and both rows, so nothing has to scroll.
+ parent.setSize(640, 400);
+ TableView view =
+ new TableView(
+ new Variables(),
+ parent,
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI,
+ columns,
+ 2,
+ modifyListener,
+ PropsUi.getInstance());
+ for (int row = 0; row < 2; row++) {
+ view.table.getItem(row).setText(1, "a" + row);
+ view.table.getItem(row).setText(2, "b" + row);
+ }
+ view.setReadonly(true);
+ return view;
+ }
+
+ /**
+ * GTK sizes a shell some time after it opens. Until then the table has no
client area and a click
+ * on any row but the first finds no visible row to land in, so wait for the
rows to show.
+ */
+ private static void waitForLayout(TableView view) {
+ long deadline = System.currentTimeMillis() + 10_000;
+ while (!onUi(() -> rowsVisible(view))) {
+ if (System.currentTimeMillis() > deadline) {
+ throw new AssertionError("the grid was never laid out: " +
onUi(view.table::getClientArea));
+ }
+ try {
+ Thread.sleep(20);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ private static boolean rowsVisible(TableView view) {
+ Rectangle clientArea = view.table.getClientArea();
+ Rectangle lastRow = view.table.getItem(view.table.getItemCount() -
1).getBounds(2);
+ return clientArea.width >= lastRow.x + lastRow.width
+ && clientArea.y + clientArea.height >= lastRow.y + lastRow.height;
+ }
+
+ private static void doubleClick(TableView view, int row, int column) {
+ sendMouse(view, SWT.MouseDown, row, column);
+ sendMouse(view, SWT.MouseDoubleClick, row, column);
+ }
+
+ private static void sendMouse(TableView view, int type, int row, int column)
{
+ Point point = center(view.table.getItem(row).getBounds(column));
+ Event event = new Event();
+ event.button = 1;
+ event.x = point.x;
+ event.y = point.y;
+ view.table.notifyListeners(type, event);
+ }
+
+ private static Point center(Rectangle bounds) {
+ return new Point(bounds.x + bounds.width / 2, bounds.y + bounds.height /
2);
+ }
+
+ 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;
+ }
+
+ private static <T> T onUi(Supplier<T> supplier) {
+ AtomicReference<T> result = new AtomicReference<>();
+ AtomicReference<RuntimeException> failure = new AtomicReference<>();
+ AtomicReference<Error> error = new AtomicReference<>();
+ display.syncExec(
+ () -> {
+ try {
+ result.set(supplier.get());
+ } catch (RuntimeException e) {
+ failure.set(e);
+ } catch (Error e) {
+ error.set(e);
+ }
+ });
+ if (failure.get() != null) {
+ throw failure.get();
+ }
+ if (error.get() != null) {
+ throw error.get();
+ }
+ return result.get();
+ }
+}
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 15af344096..7470574d21 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
@@ -183,6 +183,12 @@ public class TableView extends Composite {
private final ColumnInfo[] columns;
@Getter @Setter private boolean readonly;
+ /**
+ * Where the row context menu was last opened with the mouse, in table
coordinates, or null when
+ * it was opened from the keyboard. Set on every {@link SWT#MenuDetect},
whatever the modifiers.
+ */
+ private Point contextMenuPoint;
+
/**
* Draw long / multi-line text cells shortened and single-lined (see {@link
* #formatCellValueForDisplay(String)}). Off by default: it was added to
keep the data-heavy grids
@@ -767,6 +773,30 @@ public class TableView extends Composite {
private MouseListener createTableMouseListener() {
return new MouseAdapter() {
+ // A single click selects the row in a read-only grid, so double-click
is what opens the
+ // view-only editor: the one place a long or multi-line value can be
read in full and its
+ // text selected.
+ @Override
+ public void mouseDoubleClick(MouseEvent event) {
+ if (!readonly
+ || event.button != 1
+ || activeTableItem == null
+ || activeTableItem.isDisposed()
+ || activeTableColumn < 1) {
+ return;
+ }
+ // A click beside or below the rows leaves the active cell where it
was: only open the cell
+ // the click actually landed in.
+ if (!activeTableItem.getBounds(activeTableColumn).contains(event.x,
event.y)) {
+ return;
+ }
+ // Where the owner picks or opens the double-clicked row, that is all
a double-click does.
+ if (ownerHandlesRowActivation()) {
+ return;
+ }
+ edit(activeTableRow, activeTableColumn);
+ }
+
@Override
public void mouseDown(MouseEvent event) {
// Commit whatever cell is being edited before the click moves the
active cell somewhere
@@ -869,7 +899,7 @@ public class TableView extends Composite {
}
final int rowNr = table.indexOf(row);
final int colNr = activeTableColumn;
- if (isColumnReadOnly(colNr)) {
+ if (isViewOnly(colNr)) {
// Nothing is typed into a view-only editor, so there is nothing to
write back.
return;
}
@@ -1346,7 +1376,7 @@ public class TableView extends Composite {
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)) {
+ if (isViewOnly(colNr)) {
disposeInlineEditor(ftext, fholder);
return;
}
@@ -1480,7 +1510,6 @@ public class TableView extends Composite {
OsHelper.customizeMenuitemText(BaseMessages.getString(PKG,
"TableView.menu.SelectAll")));
miSelectAll.setImage(GuiResource.getInstance().getImageSelectAll());
miSelectAll.addListener(SWT.Selection, e -> selectAll());
- miSelectAll.setEnabled(!readonly);
}
if (!removeToolItems.contains(ID_TOOLBAR_CLEAR_SELECTION)) {
@@ -1490,7 +1519,6 @@ public class TableView extends Composite {
BaseMessages.getString(PKG, "TableView.menu.ClearSelection")));
miUnselectAll.setImage(GuiResource.getInstance().getImageUnselectAll());
miUnselectAll.addListener(SWT.Selection, e -> unselectAll());
- miUnselectAll.setEnabled(!readonly);
}
if (!removeToolItems.contains(ID_TOOLBAR_FILTERED_SELECTION)) {
@@ -1499,7 +1527,6 @@ public class TableView extends Composite {
OsHelper.customizeMenuitemText(
BaseMessages.getString(PKG,
"TableView.menu.FilteredSelection")));
miFilter.addListener(SWT.Selection, e -> setFilter());
- miFilter.setEnabled(!readonly);
}
if (!removeToolItems.contains(ID_TOOLBAR_NAVIGATE_TO_COLUMN)) {
@@ -1533,7 +1560,16 @@ public class TableView extends Composite {
BaseMessages.getString(PKG, "TableView.menu.CopyToClipboard")));
miCopy.setImage(GuiResource.getInstance().getImageCopy());
miCopy.addListener(SWT.Selection, e -> clipSelected());
- miCopy.setEnabled(!readonly);
+
+ MenuItem miCopyCell = new MenuItem(mRow, SWT.NONE);
+ miCopyCell.setText(
+ OsHelper.customizeMenuitemText(
+ BaseMessages.getString(PKG, "TableView.menu.CopyCellValue")));
+ miCopyCell.setImage(GuiResource.getInstance().getImageCopy());
+ miCopyCell.addListener(SWT.Selection, e -> clipCell());
+ mRow.addListener(
+ SWT.Show,
+ e -> miCopyCell.setEnabled(hasActiveValueCell() &&
isContextMenuOnActiveCell()));
}
if (!removeToolItems.contains(ID_TOOLBAR_PASTE_TO_TABLE)) {
@@ -2029,12 +2065,30 @@ 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();
+ /**
+ * True when the editor on the given (1-based) table column is only a
viewer: the whole grid is
+ * read-only, or that column is.
+ */
+ private boolean isViewOnly(int colNr) {
+ return readonly
+ || (colNr > 0
+ && colNr - 1 < columns.length
+ && columns[colNr - 1] != null
+ && columns[colNr - 1].isReadOnly());
+ }
+
+ /**
+ * True when the grid's owner already acts on a double-clicked row, to pick
or open it.
+ *
+ * <p>{@code table.addSelectionListener(...)} and {@code
table.addMouseListener(...)} also listen
+ * for {@link SWT#DefaultSelection} and {@link SWT#MouseDoubleClick}. An
owner that only reacts to
+ * a selection or a mouse-down should use {@code
table.addListener(SWT.Selection, ...)}, or a
+ * double-click in its read-only grid no longer opens the cell.
+ */
+ private boolean ownerHandlesRowActivation() {
+ // Our own mouse listener is one of the double-click listeners, so look
for another.
+ return table.isListening(SWT.DefaultSelection)
+ || table.getListeners(SWT.MouseDoubleClick).length > 1;
}
/** Variant for call sites that captured the editor and its holder before
going asynchronous. */
@@ -2070,7 +2124,7 @@ public class TableView extends Composite {
return;
}
// The editor on a read-only cell is a viewer: take it down again without
touching the value.
- if (isColumnReadOnly(colNr)) {
+ if (isViewOnly(colNr)) {
disposeInlineEditor();
table.setFocus();
return;
@@ -2162,11 +2216,16 @@ public class TableView extends Composite {
return;
}
- if (activeTableColumn > 0) {
+ if (activeTableColumn > 0 && !readonly) {
edit(activeTableRow, activeTableColumn);
- } else {
- selectRows(activeTableRow, activeTableRow);
+ return;
}
+
+ // A click in a read-only grid, or on the row number, selects the row.
Table.select() sends no
+ // SWT.Selection and the native click usually sends none either once the
row is selected, so
+ // refresh the toolbar here, as edit(), selectAll() and unselectAll() do.
+ selectRows(activeTableRow, activeTableRow);
+ enableToolbarButtons();
}
/**
@@ -2213,9 +2272,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();
+ // On a read-only column or grid 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 = isViewOnly(colNr);
// 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;
@@ -2912,6 +2971,49 @@ public class TableView extends Composite {
clipboard.setContents(new String[] {clip}, new Transfer[] {tran});
}
+ public void clipCell() {
+ String value = getActiveCellValueToCopy();
+ if (value != null) {
+ GuiResource.getInstance().toClipboard(value);
+ }
+ }
+
+ /**
+ * The stored value of the active cell as Copy cell value puts it on the
clipboard: the full value
+ * rather than the shortened display text, and nothing for a NULL. Null when
no value cell is
+ * active.
+ */
+ String getActiveCellValueToCopy() {
+ if (!hasActiveValueCell()) {
+ return null;
+ }
+ String value = getCellValue(activeTableItem, activeTableColumn);
+ return isNullPlaceholder(activeTableItem, activeTableColumn, value) ? "" :
value;
+ }
+
+ /** A value cell (not the row number) is active. */
+ private boolean hasActiveValueCell() {
+ return activeTableItem != null
+ && !activeTableItem.isDisposed()
+ && activeTableColumn > 0
+ && activeTableColumn < table.getColumnCount();
+ }
+
+ /**
+ * The row context menu was opened from the keyboard, or with the mouse on
the active cell. A
+ * right-click beside or below the rows, or a shift/ctrl right-click on
another cell, leaves the
+ * active cell where it was, so the menu has no cell to act on.
+ */
+ private boolean isContextMenuOnActiveCell() {
+ return contextMenuPoint == null
+ ||
activeTableItem.getBounds(activeTableColumn).contains(contextMenuPoint);
+ }
+
+ /** The cell shows the blue {@code <null>} placeholder a grid draws for a
NULL value. */
+ private boolean isNullPlaceholder(TableItem item, int colNr, String value) {
+ return "<null>".equals(value) &&
nullTextColor.equals(item.getForeground(colNr));
+ }
+
private String getSelectedText() {
StringBuilder selection = new StringBuilder();
@@ -2941,11 +3043,8 @@ public class TableView extends Composite {
selection.append(CLIPBOARD_DELIMITER);
}
String value = getCellValue(ti, c);
- if (StringUtils.isNotEmpty(value)) {
- Color textColor = ti.getForeground(c);
- if (!nullTextColor.equals(textColor) || !"<null>".equals(value)) {
- selection.append(value);
- }
+ if (StringUtils.isNotEmpty(value) && !isNullPlaceholder(ti, c, value))
{
+ selection.append(value);
}
}
selection.append(Const.CR);
@@ -3281,10 +3380,12 @@ public class TableView extends Composite {
// 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) {
+ // it. Give it the same inline editor (and expand icon), only view-only. A
read-only column that
+ // hands its click to a selection adapter keeps doing nothing, as before.
A read-only grid makes
+ // every one of its cells read-only, and there the adapter (a picker or
editor that writes the
+ // cell) is skipped for the viewer, so a long formula or expression can
still be read.
+ final boolean viewOnly = isViewOnly(colNr);
+ if (colinfo.isReadOnly() && colinfo.getSelectionAdapter() != null) {
return;
}
@@ -3299,7 +3400,7 @@ public class TableView extends Composite {
disposeInlineEditor();
}
- if (colinfo.getSelectionAdapter() != null) {
+ if (!viewOnly && colinfo.getSelectionAdapter() != null) {
Event e = new Event();
e.widget = this;
e.x = colNr;
@@ -3415,7 +3516,12 @@ public class TableView extends Composite {
textWidget.addListener(SWT.KeyUp, lsKeyUp);
}
} else {
- Text textWidget = new Text(editorParent, viewOnly ? SWT.READ_ONLY :
SWT.NONE);
+ // A view-only password stays masked: it is opened to be looked at, not
to be revealed.
+ int style = SWT.NONE;
+ if (viewOnly) {
+ style = SWT.READ_ONLY | (passwordField ? SWT.PASSWORD : SWT.NONE);
+ }
+ Text textWidget = new Text(editorParent, style);
text = textWidget;
textWidget.setText(content);
// A view-only editor never changes anything, so it takes no modify/undo
listeners.
@@ -3506,9 +3612,7 @@ public class TableView extends Composite {
expandLabel.setToolTipText(
BaseMessages.getString(
PKG,
- colinfo.isReadOnly()
- ? "TableView.tooltip.ViewValue"
- : "TableView.tooltip.ExpandValue"));
+ isViewOnly(colNr) ? "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
@@ -3608,6 +3712,12 @@ public class TableView extends Composite {
fieldChanged = false;
ColumnInfo columnInfo = columns[colNr - 1];
+ // A read-only combo column still lets the user pick another value, so a
read-only grid gets no
+ // combo at all. Its values are short and already shown in full.
+ if (readonly) {
+ return;
+ }
+
if (columnInfo.isReadOnly() && columnInfo.getSelectionAdapter() != null) {
return;
}
@@ -3727,7 +3837,7 @@ public class TableView extends Composite {
ColumnInfo columnInfo = columns[colNr - 1];
- if (columnInfo.isReadOnly()) {
+ if (readonly || columnInfo.isReadOnly()) {
return;
}
@@ -4979,12 +5089,20 @@ public class TableView extends Composite {
SWT.MenuDetect,
event -> {
Point pt = table.toControl(event.x, event.y);
- boolean onHeader = pt.y >= 0 && pt.y < table.getHeaderHeight();
+ // Measure from the client area: GTK puts the header above y 0 (its
client area starts
+ // at minus the header height), the other platforms below it. A menu
opened from the
+ // keyboard is always the row menu.
+ Rectangle clientArea = table.getClientArea();
+ boolean onHeader =
+ event.detail != SWT.MENU_KEYBOARD
+ && pt.y >= clientArea.y
+ && pt.y < clientArea.y + table.getHeaderHeight();
if (onHeader) {
headerDataColumn[0] = findDataColumnAtX(pt.x);
miHide.setEnabled(headerDataColumn[0] >= 0);
table.setMenu(headerMenu);
} else {
+ contextMenuPoint = event.detail == SWT.MENU_KEYBOARD ? null : pt;
table.setMenu(rowMenu);
}
});
diff --git
a/ui/src/main/java/org/apache/hop/ui/pipeline/debug/PipelineDebugDialog.java
b/ui/src/main/java/org/apache/hop/ui/pipeline/debug/PipelineDebugDialog.java
index b155144893..9ce2e61338 100644
--- a/ui/src/main/java/org/apache/hop/ui/pipeline/debug/PipelineDebugDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/pipeline/debug/PipelineDebugDialog.java
@@ -191,21 +191,19 @@ public class PipelineDebugDialog extends Dialog {
wTransforms.setLayoutData(fdTransform);
wTransforms.table.setHeaderVisible(false);
- // If someone clicks on a row, we want to refresh the right pane...
+ // If someone clicks on a row, we want to refresh the right pane. Listen
for the selection
+ // only: a double-click on a row is left to the grid, which opens the cell.
//
- wTransforms.table.addSelectionListener(
- new SelectionAdapter() {
-
- @Override
- public void widgetSelected(SelectionEvent e) {
- // Before we show anything, make sure to save the content of the
screen...
- //
- getTransformDebugMeta();
-
- // Now show the information...
- //
- showTransformDebugInformation();
- }
+ wTransforms.table.addListener(
+ SWT.Selection,
+ e -> {
+ // Before we show anything, make sure to save the content of the
screen...
+ //
+ getTransformDebugMeta();
+
+ // Now show the information...
+ //
+ showTransformDebugInformation();
});
// If someone presses enter, launch the pipeline (this allows for
"quick-preview")
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 181973ac92..f946ee498b 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
@@ -61,6 +61,7 @@ TableView.ErrorDialog.title=Unexpected error
TableView.menu.ClearAll=Clear all
TableView.menu.ClearSelection=Clear selection\tESC
TableView.menu.CopyFieldToAllRows=Copy field value to all rows
+TableView.menu.CopyCellValue=Copy cell value
TableView.menu.CopyToClipboard=Copy selected lines to clipboard\tCtrl-C
TableView.menu.CutSelected=Cut selected lines\tCtrl-X
TableView.menu.DeleteSelected=Delete selected lines\tDEL