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 a1a09f0c74 Issue #8147 : Stop metrics grid flicker and add column
resize controls (#8174)
a1a09f0c74 is described below
commit a1a09f0c7430a7a9c679de7844540079b4faad42
Author: Matt Casters <[email protected]>
AuthorDate: Sat Aug 29 20:27:51 2026 +0200
Issue #8147 : Stop metrics grid flicker and add column resize controls
(#8174)
Win32 was packing and restoring column widths on every 1s metrics refresh
(and TableView.optWidth applied setWidth twice). Timer ticks now grow
auto-sized columns only; user-dragged widths stick.
View menu and GUI options gain Dynamic column resizing (default on) and
Fit columns to content so users can freeze the layout on Windows 11.
---
.../hop/ui/core/widget/TableViewOptWidthTest.java | 215 ++++++++++++++
.../main/java/org/apache/hop/ui/core/PropsUi.java | 14 +
.../org/apache/hop/ui/core/widget/TableView.java | 263 +++++++++++------
.../delegates/HopGuiPipelineGridDelegate.java | 314 +++++++++++++++------
.../configuration/tabs/ConfigGuiOptionsTab.java | 19 ++
.../core/dialog/messages/messages_en_US.properties | 2 +
.../ui/hopgui/messages/messages_en_US.properties | 6 +-
7 files changed, 657 insertions(+), 176 deletions(-)
diff --git
a/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewOptWidthTest.java
b/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewOptWidthTest.java
new file mode 100644
index 0000000000..60baf47bc7
--- /dev/null
+++ b/rcp/src/test/java/org/apache/hop/ui/core/widget/TableViewOptWidthTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.assertTrue;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Width-stability contracts for {@link TableView#optWidth}. Live metrics
refresh used to set each
+ * column twice (and bounce pack-then-restore); these cases pin down the
behaviour Windows 11 needs
+ * even though this agent runs under GTK.
+ */
+@Tag("uitest")
+class TableViewOptWidthTest extends SwtBotTestBase {
+
+ private static final int NAME_COLUMN = 1;
+ private static final int VALUE_COLUMN = 2;
+ private static final int PREFERRED_WIDTH = 250;
+
+ @Test
+ void optWidthTwiceWithUnchangedContentLeavesColumnWidthsAlone() {
+ withTableView(
+ (view, bot) -> {
+ // Pack after the shell is open: GTK stretches the last column on
first layout.
+ onUi(
+ () -> {
+ view.optWidth(true);
+ return null;
+ });
+ int[] first = snapshotWidths(view);
+ onUi(
+ () -> {
+ view.optWidth(true);
+ return null;
+ });
+ int[] second = snapshotWidths(view);
+ assertEquals(first.length, second.length);
+ for (int i = 0; i < first.length; i++) {
+ assertEquals(first[i], second[i], "column " + i + " must not move
on a no-op pack");
+ }
+ });
+ }
+
+ @Test
+ void optWidthHonorsExplicitColumnWidth() {
+ withTableView(
+ (view, bot) -> {
+ onUi(
+ () -> {
+ view.setPreferredColumnWidth(VALUE_COLUMN, PREFERRED_WIDTH);
+ view.optWidth(true);
+ return null;
+ });
+ assertEquals(PREFERRED_WIDTH, columnWidth(view, VALUE_COLUMN));
+ onUi(
+ () -> {
+ view.optWidth(true);
+ return null;
+ });
+ assertEquals(
+ PREFERRED_WIDTH,
+ columnWidth(view, VALUE_COLUMN),
+ "a second pack must not override the preferred width");
+ });
+ }
+
+ @Test
+ void growOnlyWidensForLongerTextButNeverShrinks() {
+ withTableView(
+ (view, bot) -> {
+ onUi(
+ () -> {
+ view.table.getItem(0).setText(VALUE_COLUMN, "1");
+ view.optWidth(true);
+ return null;
+ });
+ int packedShort = columnWidth(view, VALUE_COLUMN);
+
+ onUi(
+ () -> {
+ view.table.getItem(0).setText(VALUE_COLUMN,
"1,234,567,890,123");
+ view.optWidth(true, 0, true);
+ return null;
+ });
+ int afterGrow = columnWidth(view, VALUE_COLUMN);
+ assertTrue(
+ afterGrow > packedShort,
+ "grow-only should widen for a much longer value (short="
+ + packedShort
+ + ", grown="
+ + afterGrow
+ + ")");
+
+ onUi(
+ () -> {
+ view.table.getItem(0).setText(VALUE_COLUMN, "1");
+ view.optWidth(true, 0, true);
+ return null;
+ });
+ assertEquals(
+ afterGrow,
+ columnWidth(view, VALUE_COLUMN),
+ "grow-only must not shrink when the cell text gets shorter");
+ });
+ }
+
+ @Test
+ void growOnlyDoesNotTouchUserSizedColumns() {
+ withTableView(
+ (view, bot) -> {
+ onUi(
+ () -> {
+ view.setPreferredColumnWidth(VALUE_COLUMN, PREFERRED_WIDTH);
+ view.optWidth(true);
+ view.table.getItem(0).setText(VALUE_COLUMN,
"1,234,567,890,123");
+ view.optWidth(true, 0, true);
+ return null;
+ });
+ assertEquals(
+ PREFERRED_WIDTH,
+ columnWidth(view, VALUE_COLUMN),
+ "grow-only must leave a user-sized column alone");
+ });
+ }
+
+ private void withTableView(BiConsumer<TableView, SWTBot> body) {
+ AtomicReference<TableView> viewRef = new AtomicReference<>();
+ withScene(
+ shell -> {
+ shell.setLayout(new FillLayout());
+ shell.setSize(700, 280);
+ ColumnInfo[] columns = {
+ new ColumnInfo("Name", ColumnInfo.COLUMN_TYPE_TEXT, false, true),
+ new ColumnInfo("Value", ColumnInfo.COLUMN_TYPE_TEXT, true, true),
+ };
+ TableView view =
+ new TableView(
+ new Variables(),
+ shell,
+ SWT.BORDER | SWT.FULL_SELECTION,
+ columns,
+ 1,
+ true,
+ null,
+ PropsUi.getInstance());
+ TableItem row = view.table.getItem(0);
+ row.setText(NAME_COLUMN, "rows");
+ row.setText(VALUE_COLUMN, "1");
+ view.optWidth(true);
+ viewRef.set(view);
+ },
+ bot -> body.accept(viewRef.get(), bot));
+ }
+
+ private static int[] snapshotWidths(TableView view) {
+ return onUi(
+ () -> {
+ int n = view.table.getColumnCount();
+ int[] widths = new int[n];
+ for (int i = 0; i < n; i++) {
+ widths[i] = view.table.getColumn(i).getWidth();
+ }
+ return widths;
+ });
+ }
+
+ private static int columnWidth(TableView view, int index) {
+ return onUi(() -> view.table.getColumn(index).getWidth());
+ }
+
+ private static <T> T onUi(Supplier<T> supplier) {
+ AtomicReference<T> result = new AtomicReference<>();
+ AtomicReference<RuntimeException> failure = new AtomicReference<>();
+ display.syncExec(
+ () -> {
+ try {
+ result.set(supplier.get());
+ } catch (RuntimeException e) {
+ failure.set(e);
+ }
+ });
+ if (failure.get() != null) {
+ throw failure.get();
+ }
+ return result.get();
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
index 9c86b941e9..8202d76abb 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
@@ -116,6 +116,8 @@ public class PropsUi extends Props {
private static final String METRICS_PANEL_SHOW_DATA_VOLUME =
"MetricsPanel.ShowDataVolume";
private static final String METRICS_PANEL_SHOW_DATA_VOLUME_IN =
"MetricsPanel.ShowDataVolumeIn";
private static final String METRICS_PANEL_SHOW_DATA_VOLUME_OUT =
"MetricsPanel.ShowDataVolumeOut";
+ private static final String METRICS_PANEL_DYNAMIC_COLUMN_RESIZE =
+ "MetricsPanel.DynamicColumnResize";
public static final int DEFAULT_MAX_EXECUTION_LOGGING_TEXT_SIZE = 2000000;
private Map<RGB, RGB> contrastingColors;
@@ -740,6 +742,18 @@ public class PropsUi extends Props {
setProperty(METRICS_PANEL_SHOW_DATA_VOLUME_OUT, show ? YES : NO);
}
+ /**
+ * When true (default), auto-sized metrics columns grow during execution as
values get wider. When
+ * false, widths stay where they were after the last pack or user drag.
+ */
+ public boolean isMetricsPanelDynamicColumnResize() {
+ return
YES.equalsIgnoreCase(getProperty(METRICS_PANEL_DYNAMIC_COLUMN_RESIZE, YES));
+ }
+
+ public void setMetricsPanelDynamicColumnResize(boolean dynamic) {
+ setProperty(METRICS_PANEL_DYNAMIC_COLUMN_RESIZE, dynamic ? YES : NO);
+ }
+
public static void setLook(Widget widget) {
int style = WIDGET_STYLE_DEFAULT;
if (widget instanceof Table) {
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 9acd521206..b692d763e0 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
@@ -267,6 +267,13 @@ public class TableView extends Composite {
private final Set<Integer> hiddenDataColumns = new HashSet<>();
private int[] rememberedWidths;
+ /**
+ * Last width we asked each native column to take during {@link #optWidth}.
Win32/DPI often
+ * returns a slightly different {@code getWidth()} than the value passed to
{@code setWidth}, so
+ * grow-only mode uses this to avoid applying the same target again every
refresh.
+ */
+ private int[] lastOptWidthApplied;
+
/**
* Create a table to add to a dialog
*
@@ -3683,6 +3690,21 @@ public class TableView extends Composite {
}
public void optWidth(boolean header, int nrLines) {
+ optWidth(header, nrLines, false);
+ }
+
+ /**
+ * Size columns to their content.
+ *
+ * @param header include header text in the packed size
+ * @param nrLines max rows to measure, or {@code <= 0} for all rows
+ * @param growOnly when true, never shrink a column and never touch columns
with an explicit
+ * {@link ColumnInfo} width (user-sized). Used by live grids that
refresh often.
+ */
+ public void optWidth(boolean header, int nrLines, boolean growOnly) {
+ if (table == null || table.isDisposed()) {
+ return;
+ }
int extraForMargin;
if (Const.isWindows()) {
@@ -3692,117 +3714,130 @@ public class TableView extends Composite {
}
extraForMargin += EXTRA_COLUMN_WIDTH_MARGIN;
- for (int c = 0; c < table.getColumnCount(); c++) {
- TableColumn tc = table.getColumn(c);
- if (c > 0 && hiddenDataColumns.contains(c - 1)) {
- if (tc.getWidth() != 0) {
- tc.setWidth(0);
- }
- continue;
- }
- int max = 0;
- if (header) {
- max = TextSizeUtilFacade.textExtent(tc.getText()).x + extraForMargin;
- if (tc.getImage() != null) {
- max += tc.getImage().getBounds().width;
+ boolean widthChanged = false;
+ table.setRedraw(false);
+ try {
+ for (int c = 0; c < table.getColumnCount(); c++) {
+ TableColumn tc = table.getColumn(c);
+ if (c > 0 && hiddenDataColumns.contains(c - 1)) {
+ if (tc.getWidth() != 0) {
+ tc.setWidth(0);
+ rememberAppliedColumnWidth(c, 0);
+ widthChanged = true;
+ }
+ continue;
}
+ int max = 0;
+ if (header) {
+ max = TextSizeUtilFacade.textExtent(tc.getText()).x + extraForMargin;
+ if (tc.getImage() != null) {
+ max += tc.getImage().getBounds().width;
+ }
- // Check if the column has a sorted mark set. In that case, we need the
- // header to be a bit wider...
- //
- if (c == sortField && sortable) {
- max += ConstUi.SMALL_ICON_SIZE + extraForMargin;
+ // Check if the column has a sorted mark set. In that case, we need
the
+ // header to be a bit wider...
+ //
+ if (c == sortField && sortable) {
+ max += ConstUi.SMALL_ICON_SIZE + extraForMargin;
+ }
}
- }
- Set<String> columnStrings = new HashSet<>();
-
- boolean haveToGetTexts = false;
- if (c > 0) {
- final ColumnInfo column = columns[c - 1];
- if (column != null) {
- switch (column.getType()) {
- case ColumnInfo.COLUMN_TYPE_TEXT_BUTTON,
ColumnInfo.COLUMN_TYPE_TEXT:
- haveToGetTexts = true;
- break;
- case ColumnInfo.COLUMN_TYPE_CCOMBO, ColumnInfo.COLUMN_TYPE_FORMAT:
- haveToGetTexts = true;
- if (column.getComboValues() != null) {
- for (String comboValue : columns[c - 1].getComboValues()) {
- columnStrings.add(comboValue);
+ Set<String> columnStrings = new HashSet<>();
+
+ boolean haveToGetTexts = false;
+ if (c > 0) {
+ final ColumnInfo column = columns[c - 1];
+ if (column != null) {
+ switch (column.getType()) {
+ case ColumnInfo.COLUMN_TYPE_TEXT_BUTTON,
ColumnInfo.COLUMN_TYPE_TEXT:
+ haveToGetTexts = true;
+ break;
+ case ColumnInfo.COLUMN_TYPE_CCOMBO,
ColumnInfo.COLUMN_TYPE_FORMAT:
+ haveToGetTexts = true;
+ if (column.getComboValues() != null) {
+ for (String comboValue : columns[c - 1].getComboValues()) {
+ columnStrings.add(comboValue);
+ }
}
- }
- break;
- case ColumnInfo.COLUMN_TYPE_BUTTON:
- columnStrings.add(column.getButtonText());
- break;
- default:
- break;
+ break;
+ case ColumnInfo.COLUMN_TYPE_BUTTON:
+ columnStrings.add(column.getButtonText());
+ break;
+ default:
+ break;
+ }
}
+ } else {
+ haveToGetTexts = true;
}
- } else {
- haveToGetTexts = true;
- }
- if (haveToGetTexts) {
- for (int r = 0; r < table.getItemCount() && (r < nrLines || nrLines <=
0); r++) {
- TableItem ti = table.getItem(r);
- if (ti != null) {
- // 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));
+ if (haveToGetTexts) {
+ for (int r = 0; r < table.getItemCount() && (r < nrLines || nrLines
<= 0); r++) {
+ TableItem ti = table.getItem(r);
+ if (ti != null) {
+ // 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));
+ }
}
}
- }
- for (String str : columnStrings) {
- int len = TextSizeUtilFacade.textExtent(str == null ? "" : str).x;
- if (len > max) {
- max = len;
+ for (String str : columnStrings) {
+ int len = TextSizeUtilFacade.textExtent(str == null ? "" : str).x;
+ if (len > max) {
+ max = len;
+ }
}
- }
- try {
- max += extraForMargin;
- if (c > 0) {
- max += extraForMargin; // margins on both sides of the column
- }
- if (Const.isWindows() || Const.isLinux()) {
+ try {
max += extraForMargin;
- }
+ if (c > 0) {
+ max += extraForMargin; // margins on both sides of the column
+ }
+ if (Const.isWindows() || Const.isLinux()) {
+ max += extraForMargin;
+ }
- // The line number column
- //
- if (c == 0) {
- if (tc.getWidth() != max) {
- tc.setWidth(max);
+ int desiredWidth = preferredColumnWidth(c);
+ int target;
+ if (c == 0) {
+ // Line-number column: always pack unless the caller marked a
preferred width.
+ target = desiredWidth > 0 ? desiredWidth : max;
+ } else if (desiredWidth > 0) {
+ target = desiredWidth;
+ } else {
+ target = max;
}
- } else {
- int desiredWidth = columns[c - 1].getWidth();
- if (desiredWidth > 0) {
- if (tc.getWidth() != desiredWidth) {
- tc.setWidth(desiredWidth);
+
+ if (growOnly) {
+ if (desiredWidth > 0) {
+ continue;
}
- } else {
- if (tc.getWidth() != max) {
- tc.setWidth(max);
+ int baseline = Math.max(tc.getWidth(), lastAppliedColumnWidth(c));
+ if (max <= baseline) {
+ continue;
}
+ target = max;
}
- }
- if (tc.getWidth() != max) {
- if (c > 0 && columns[c - 1].getWidth() > 0) {
- tc.setWidth(columns[c - 1].getWidth());
+ if (tc.getWidth() != target) {
+ tc.setWidth(target);
+ rememberAppliedColumnWidth(c, target);
+ widthChanged = true;
} else {
- tc.setWidth(max);
+ rememberAppliedColumnWidth(c, target);
}
+ } catch (Exception e) {
+ // Ignore errors
+ LogChannel.UI.logError("error in TableView", e);
}
- } catch (Exception e) {
- // Ignore errors
- LogChannel.UI.logError("error in TableView", e);
+ }
+ } finally {
+ if (!table.isDisposed()) {
+ table.setRedraw(true);
}
}
- if (table.isListening(SWT.Resize)) {
+ if (widthChanged && table.isListening(SWT.Resize)) {
Event resizeEvent = new Event();
resizeEvent.widget = table;
resizeEvent.type = SWT.Resize;
@@ -3813,6 +3848,60 @@ public class TableView extends Composite {
unEdit();
}
+ /**
+ * Records a preferred width for a table column ({@code 0} is the "#" index
column). Later {@link
+ * #optWidth} calls honor this instead of packing, and grow-only mode will
not change it.
+ *
+ * @param tableColumnIndex native table column index
+ * @param width pixel width
+ */
+ public void setPreferredColumnWidth(int tableColumnIndex, int width) {
+ if (tableColumnIndex == 0) {
+ if (numberColumn != null) {
+ numberColumn.setWidth(width);
+ }
+ return;
+ }
+ int dataIndex = tableColumnIndex - 1;
+ if (dataIndex >= 0 && dataIndex < columns.length) {
+ columns[dataIndex].setWidth(width);
+ }
+ }
+
+ private int preferredColumnWidth(int tableColumnIndex) {
+ if (tableColumnIndex == 0) {
+ return numberColumn != null ? numberColumn.getWidth() : -1;
+ }
+ int dataIndex = tableColumnIndex - 1;
+ if (dataIndex >= 0 && dataIndex < columns.length && columns[dataIndex] !=
null) {
+ return columns[dataIndex].getWidth();
+ }
+ return -1;
+ }
+
+ private int lastAppliedColumnWidth(int tableColumnIndex) {
+ if (lastOptWidthApplied == null || tableColumnIndex >=
lastOptWidthApplied.length) {
+ return -1;
+ }
+ return lastOptWidthApplied[tableColumnIndex];
+ }
+
+ private void rememberAppliedColumnWidth(int tableColumnIndex, int width) {
+ int count = table.getColumnCount();
+ if (lastOptWidthApplied == null || lastOptWidthApplied.length != count) {
+ int[] next = new int[count];
+ Arrays.fill(next, -1);
+ if (lastOptWidthApplied != null) {
+ System.arraycopy(
+ lastOptWidthApplied, 0, next, 0,
Math.min(lastOptWidthApplied.length, count));
+ }
+ lastOptWidthApplied = next;
+ }
+ if (tableColumnIndex >= 0 && tableColumnIndex <
lastOptWidthApplied.length) {
+ lastOptWidthApplied[tableColumnIndex] = width;
+ }
+ }
+
public void optimizeTableView() {
removeEmptyRows();
setRowNums();
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
index 1c0bdf8404..43ab143b95 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
@@ -20,6 +20,7 @@ package org.apache.hop.ui.hopgui.file.pipeline.delegates;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
@@ -74,7 +75,6 @@ import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
-import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Table;
@@ -104,6 +104,8 @@ public class HopGuiPipelineGridDelegate {
public static final long UPDATE_TIME_VIEW = 1000L;
+ private static final int METRICS_TABLE_STYLE = SWT.BORDER |
SWT.FULL_SELECTION | SWT.MULTI;
+
private final HopGui hopGui;
private final HopGuiPipelineGraph pipelineGraph;
@@ -156,11 +158,17 @@ public class HopGuiPipelineGridDelegate {
private final Map<String, Integer> metricsColumnWidths = new HashMap<>();
/**
- * True while we are programmatically restoring column widths. Resize
listeners must not persist
+ * True while we are programmatically changing column widths. Resize
listeners must not persist
* those changes or they overwrite the user's saved widths.
*/
private boolean restoringColumnWidths = false;
+ /**
+ * When true, the next {@link #refreshView()} fully packs columns (View menu
toggled units or
+ * visibility). Steady-state timer ticks only grow auto-sized columns.
+ */
+ private boolean packColumnsOnNextRefresh = false;
+
/**
* @param hopGui Hop GUI instance
* @param pipelineGraph the pipeline graph that owns this delegate
@@ -196,6 +204,7 @@ public class HopGuiPipelineGridDelegate {
gridSortColumn = 0;
gridSortDescending = false;
lastEngineMetrics = null;
+ packColumnsOnNextRefresh = true;
startRefreshMetricsTimer();
return;
}
@@ -304,33 +313,12 @@ public class HopGuiPipelineGridDelegate {
columns[12].setAlignment(SWT.RIGHT);
columns[13].setAlignment(SWT.RIGHT);
- pipelineGridView =
- new TableView(
- pipelineGraph.getVariables(),
- pipelineGridComposite,
- SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI,
- columns,
- 1,
- true, // readonly
- null,
- hopGui.getProps(),
- true,
- null,
- false,
- false); // no TableView toolbar; copy/filter are on our toolbar
- FormData fdView = new FormData();
- fdView.left = new FormAttachment(0, 0);
- fdView.right = new FormAttachment(100, 0);
- fdView.top = new FormAttachment(toolbar, 0);
- fdView.bottom = new FormAttachment(100, 0);
- pipelineGridView.setLayoutData(fdView);
- pipelineGridView.setSortable(true);
+ pipelineGridView = createMetricsTableView(columns, 1);
+ metricsColumnKeys = placeholderColumnKeys();
+ attachColumnWidthListeners();
attachMetricsTableListeners(pipelineGridView);
-
- ColumnInfo numberColumn = pipelineGridView.getNumberColumn();
- IValueMeta numberColumnValueMeta =
- new ValueMetaString("#",
HopGuiPipelineGridDelegate::subTransformCompare);
- numberColumn.setValueMeta(numberColumnValueMeta);
+ applyNumberColumnComparator();
+ previousRefreshColumns = Arrays.asList(columns);
startRefreshMetricsTimer();
pipelineGridTab.addDisposeListener(disposeEvent ->
stopRefreshMetricsTimer());
@@ -423,9 +411,10 @@ public class HopGuiPipelineGridDelegate {
}
/**
- * Add a "View" dropdown to the toolbar with checkable items for metrics
panel options (hide
- * units, hide columns). Syncs with PropsUi so options dialog and toolbar
stay in sync. Uses a
- * CLabel with SWT.CENTER so the text is centered consistently across
platforms (e.g. Linux).
+ * Add a "View" dropdown to the toolbar with checkable items for metrics
panel options (units,
+ * column resizing, hide columns). Syncs with PropsUi so options dialog and
toolbar stay in sync.
+ * Uses a CLabel with SWT.CENTER so the text is centered consistently across
platforms (e.g.
+ * Linux).
*/
private void addMetricsViewDropdown(Control toolbarControl) {
if (!(toolbarControl instanceof ToolBar tb)) {
@@ -465,6 +454,30 @@ public class HopGuiPipelineGridDelegate {
setMetricsOptionAndRefresh(
() ->
props.setMetricsPanelShowUnits(showUnitsItem.getSelection())));
+ MenuItem dynamicResizeItem = new MenuItem(menu, SWT.CHECK);
+ dynamicResizeItem.setText(
+ BaseMessages.getString(PKG,
"PipelineLog.MetricsView.DynamicColumnResize"));
+ dynamicResizeItem.setToolTipText(
+ BaseMessages.getString(PKG,
"PipelineLog.MetricsView.DynamicColumnResize.Tooltip"));
+ dynamicResizeItem.setSelection(props.isMetricsPanelDynamicColumnResize());
+ dynamicResizeItem.addListener(
+ SWT.Selection,
+ e ->
+ setMetricsOptionAndRefresh(
+ () ->
props.setMetricsPanelDynamicColumnResize(dynamicResizeItem.getSelection()),
+ false));
+
+ MenuItem fitColumnsItem = new MenuItem(menu, SWT.PUSH);
+ fitColumnsItem.setText(BaseMessages.getString(PKG,
"PipelineLog.MetricsView.FitColumns"));
+ fitColumnsItem.setToolTipText(
+ BaseMessages.getString(PKG,
"PipelineLog.MetricsView.FitColumns.Tooltip"));
+ fitColumnsItem.addListener(
+ SWT.Selection,
+ e -> {
+ packColumnsOnNextRefresh = true;
+ refreshView();
+ });
+
new MenuItem(menu, SWT.SEPARATOR);
addMetricsViewMenuItem(
@@ -546,12 +559,17 @@ public class HopGuiPipelineGridDelegate {
}
private void setMetricsOptionAndRefresh(Runnable setOption) {
+ setMetricsOptionAndRefresh(setOption, true);
+ }
+
+ private void setMetricsOptionAndRefresh(Runnable setOption, boolean
packColumns) {
setOption.run();
try {
HopConfig.getInstance().saveToFile();
} catch (Exception ignored) {
// best-effort persist
}
+ packColumnsOnNextRefresh = packColumns;
refreshView();
}
@@ -678,21 +696,37 @@ public class HopGuiPipelineGridDelegate {
List<List<String>> componentStringsList =
buildComponentStringsList(engineMetrics, shownComponents,
usedMetrics);
- recreateTableIfColumnsChanged(columns, shownComponents.size(),
usedMetrics);
+ boolean recreated =
+ recreateTableIfColumnsChanged(columns, shownComponents.size(),
usedMetrics);
+ boolean packColumns = packColumnsOnNextRefresh;
+ packColumnsOnNextRefresh = false;
sortComponentStringsByColumn(componentStringsList, gridSortColumn,
gridSortDescending);
+ Table table = pipelineGridView.table;
+ int topIndex = table.getTopIndex();
+ int[] selection = table.getSelectionIndices();
+
int errorCol = indexOfMetric(usedMetrics, Pipeline.METRIC_ERROR);
fillTableRows(componentStringsList, errorCol);
setSortIndicator();
- // Ignore resize events from optWidth and restoreColumnWidths so we
don't overwrite saved
- // widths
+ // Ignore resize events from optWidth / restore so we don't treat them
as user drags.
restoringColumnWidths = true;
- pipelineGridView.optWidth(true);
- restoreColumnWidths(usedMetrics);
+ try {
+ if (packColumns && !recreated) {
+ pipelineGridView.optWidth(true);
+ restoreColumnWidths(usedMetrics);
+ } else if (PropsUi.getInstance().isMetricsPanelDynamicColumnResize()) {
+ // Recreate already packed in the constructor; timer ticks only grow
auto-sized columns.
+ pipelineGridView.optWidth(true, 0, true);
+ }
+ } finally {
+ restoringColumnWidths = false;
+ }
previousRefreshColumns = columns;
+ restoreTableViewport(table, topIndex, selection);
updateEditButtonState();
} finally {
refreshViewLock.unlock();
@@ -918,39 +952,100 @@ public class HopGuiPipelineGridDelegate {
return componentStringsList;
}
- private void recreateTableIfColumnsChanged(
+ /**
+ * Rebuilds the metrics {@link TableView} when the column set changes.
Returns true when a new
+ * table was created (constructor already packed columns).
+ */
+ private boolean recreateTableIfColumnsChanged(
List<ColumnInfo> columns, int rowCount, List<IEngineMetric> usedMetrics)
{
if (!haveColumnsChanged(columns)) {
- return;
+ return false;
}
saveColumnWidths();
pipelineGridView.dispose();
- pipelineGridView =
+ pipelineGridView = createMetricsTableView(columns.toArray(new
ColumnInfo[0]), rowCount);
+ metricsColumnKeys = getColumnKeys(usedMetrics);
+ applyNumberColumnComparator();
+ attachColumnWidthListeners();
+ attachMetricsTableListeners(pipelineGridView);
+ pipelineGridComposite.layout(true, true);
+ restoreColumnWidths(usedMetrics);
+ return true;
+ }
+
+ private TableView createMetricsTableView(ColumnInfo[] columns, int rowCount)
{
+ TableView view =
new TableView(
pipelineGraph.getVariables(),
pipelineGridComposite,
- SWT.NONE,
- columns.toArray(new ColumnInfo[0]),
- rowCount,
+ METRICS_TABLE_STYLE,
+ columns,
+ Math.max(rowCount, 1),
true,
null,
- PropsUi.getInstance(),
+ hopGui.getProps(),
true,
null,
false,
false); // no TableView toolbar; copy/filter are on our toolbar
- pipelineGridView.setSortable(true);
- metricsColumnKeys = getColumnKeys(usedMetrics);
- attachColumnWidthListeners();
- attachMetricsTableListeners(pipelineGridView);
+ view.setSortable(true);
FormData fdView = new FormData();
fdView.left = new FormAttachment(0, 0);
fdView.right = new FormAttachment(100, 0);
fdView.top = new FormAttachment(toolbar, 0);
fdView.bottom = new FormAttachment(100, 0);
- pipelineGridView.setLayoutData(fdView);
- pipelineGridComposite.layout(true, true);
- restoreColumnWidths(usedMetrics);
+ view.setLayoutData(fdView);
+ return view;
+ }
+
+ private void applyNumberColumnComparator() {
+ if (pipelineGridView == null || pipelineGridView.isDisposed()) {
+ return;
+ }
+ ColumnInfo numberColumn = pipelineGridView.getNumberColumn();
+ IValueMeta numberColumnValueMeta =
+ new ValueMetaString("#",
HopGuiPipelineGridDelegate::subTransformCompare);
+ numberColumn.setValueMeta(numberColumnValueMeta);
+ }
+
+ /**
+ * Column keys used by the placeholder table created in {@link
#addPipelineGrid()} before the
+ * first engine-metrics refresh rebuilds the real column set.
+ */
+ private static List<String> placeholderColumnKeys() {
+ List<String> keys = new ArrayList<>();
+ keys.add("#");
+ keys.add("TransformName");
+ keys.add("Copy");
+ keys.add(Pipeline.METRIC_NAME_INPUT);
+ keys.add(Pipeline.METRIC_NAME_READ);
+ keys.add(Pipeline.METRIC_NAME_WRITTEN);
+ keys.add(Pipeline.METRIC_NAME_OUTPUT);
+ keys.add(Pipeline.METRIC_NAME_UPDATED);
+ keys.add(Pipeline.METRIC_NAME_REJECTED);
+ keys.add(Pipeline.METRIC_NAME_ERROR);
+ keys.add(Pipeline.METRIC_NAME_BUFFER_IN);
+ keys.add(Pipeline.METRIC_NAME_BUFFER_OUT);
+ keys.add("duration");
+ keys.add("speed");
+ keys.add("status");
+ return keys;
+ }
+
+ private void restoreTableViewport(Table table, int topIndex, int[]
selection) {
+ if (table == null || table.isDisposed()) {
+ return;
+ }
+ int itemCount = table.getItemCount();
+ if (selection != null && selection.length > 0) {
+ int[] valid = Arrays.stream(selection).filter(i -> i >= 0 && i <
itemCount).toArray();
+ if (valid.length > 0) {
+ table.setSelection(valid);
+ }
+ }
+ if (topIndex >= 0 && topIndex < itemCount) {
+ table.setTopIndex(topIndex);
+ }
}
/**
@@ -971,54 +1066,65 @@ public class HopGuiPipelineGridDelegate {
return keys;
}
- /** Saves current column widths to the session map so they can be restored
after refresh. */
+ /**
+ * Refreshes native widths for columns the user has already resized.
Auto-sized columns are not
+ * stored so grow-only packing can still widen them as counts grow.
+ */
private void saveColumnWidths() {
if (pipelineGridView == null || pipelineGridView.isDisposed()) {
return;
}
- if (metricsColumnKeys == null || metricsColumnKeys.size() == 0) {
+ if (metricsColumnKeys == null || metricsColumnKeys.isEmpty() ||
metricsColumnWidths.isEmpty()) {
return;
}
- org.eclipse.swt.widgets.Table table = pipelineGridView.table;
+ Table table = pipelineGridView.table;
int n = Math.min(table.getColumnCount(), metricsColumnKeys.size());
for (int i = 0; i < n; i++) {
+ String key = metricsColumnKeys.get(i);
+ if (!metricsColumnWidths.containsKey(key)) {
+ continue;
+ }
int w = table.getColumn(i).getWidth();
if (w > 0) {
- metricsColumnWidths.put(metricsColumnKeys.get(i), w);
+ metricsColumnWidths.put(key, w);
}
}
}
/**
- * Restores column widths from the session map onto the table. Needed for
the "#" column (no
- * ColumnInfo) and as a fallback so widths are applied after optWidth.
+ * Restores user-resized column widths from the session map onto the table.
Needed for the "#"
+ * column (no data {@link ColumnInfo}) and after a table recreate.
*/
private void restoreColumnWidths(List<IEngineMetric> usedMetrics) {
if (pipelineGridView == null || pipelineGridView.isDisposed()) {
return;
}
+ if (metricsColumnWidths.isEmpty()) {
+ return;
+ }
+ boolean nested = restoringColumnWidths;
restoringColumnWidths = true;
+ Table table = pipelineGridView.table;
+ table.setRedraw(false);
try {
List<String> keys = getColumnKeys(usedMetrics);
- Table table = pipelineGridView.table;
int n = Math.min(table.getColumnCount(), keys.size());
for (int i = 0; i < n; i++) {
String key = keys.get(i);
Integer w = metricsColumnWidths.get(key);
if (w != null && w > 0) {
TableColumn tc = table.getColumn(i);
- tc.setWidth(w);
+ if (tc.getWidth() != w) {
+ tc.setWidth(w);
+ }
+ pipelineGridView.setPreferredColumnWidth(i, w);
}
}
} finally {
- // Clear flag asynchronously so any Resize events queued by setWidth()
are still ignored
- Display display =
- pipelineGridView != null && !pipelineGridView.isDisposed()
- ? pipelineGridView.table.getDisplay()
- : null;
- if (display != null && !display.isDisposed()) {
- display.asyncExec(() -> restoringColumnWidths = false);
- } else {
+ if (!table.isDisposed()) {
+ table.setRedraw(true);
+ }
+ if (!nested) {
restoringColumnWidths = false;
}
}
@@ -1029,12 +1135,13 @@ public class HopGuiPipelineGridDelegate {
if (pipelineGridView == null || pipelineGridView.isDisposed()) {
return;
}
- if (metricsColumnKeys == null || metricsColumnKeys.size() == 0) {
+ if (metricsColumnKeys == null || metricsColumnKeys.isEmpty()) {
return;
}
Table table = pipelineGridView.table;
for (int i = 0; i < table.getColumnCount() && i <
metricsColumnKeys.size(); i++) {
final String key = metricsColumnKeys.get(i);
+ final int columnIndex = i;
TableColumn col = table.getColumn(i);
col.addListener(
SWT.Resize,
@@ -1043,32 +1150,56 @@ public class HopGuiPipelineGridDelegate {
return;
}
if (!col.isDisposed()) {
- metricsColumnWidths.put(key, col.getWidth());
+ int w = col.getWidth();
+ if (w > 0) {
+ metricsColumnWidths.put(key, w);
+ pipelineGridView.setPreferredColumnWidth(columnIndex, w);
+ }
}
});
}
}
private void fillTableRows(List<List<String>> componentStringsList, int
errorColumnIndex) {
- while (pipelineGridView.table.getItemCount() >
componentStringsList.size()) {
- pipelineGridView.table.remove(pipelineGridView.table.getItemCount() - 1);
- }
- int errorsCol = 3 + errorColumnIndex; // row has #, name, copy, then
metrics
- Color errorBg = GuiResource.getInstance().getColorLightRed();
- for (int row = 0; row < componentStringsList.size(); row++) {
- List<String> componentStrings = componentStringsList.get(row);
- TableItem item;
- if (row < pipelineGridView.table.getItemCount()) {
- item = pipelineGridView.table.getItem(row);
- } else {
- item = new TableItem(pipelineGridView.table, SWT.NONE);
+ Table table = pipelineGridView.table;
+ table.setRedraw(false);
+ try {
+ while (table.getItemCount() > componentStringsList.size()) {
+ table.remove(table.getItemCount() - 1);
+ }
+ int errorsCol = 3 + errorColumnIndex; // row has #, name, copy, then
metrics
+ Color errorBg = GuiResource.getInstance().getColorLightRed();
+ for (int row = 0; row < componentStringsList.size(); row++) {
+ List<String> componentStrings = componentStringsList.get(row);
+ TableItem item;
+ if (row < table.getItemCount()) {
+ item = table.getItem(row);
+ } else {
+ item = new TableItem(table, SWT.NONE);
+ }
+ applyRowTextsIfChanged(item, componentStrings);
+ if (errorColumnIndex >= 0 && errorsCol < componentStrings.size()) {
+ long err = parseFormattedLong(componentStrings.get(errorsCol));
+ boolean wantError = err > 0;
+ boolean hasError = errorBg.equals(item.getBackground());
+ if (wantError != hasError) {
+ item.setBackground(wantError ? errorBg : null);
+ }
+ }
}
- for (int col = 0; col < componentStrings.size(); col++) {
- item.setText(col, componentStrings.get(col));
+ } finally {
+ if (!table.isDisposed()) {
+ table.setRedraw(true);
}
- if (errorColumnIndex >= 0 && errorsCol < componentStrings.size()) {
- long err = parseFormattedLong(componentStrings.get(errorsCol));
- item.setBackground(err > 0 ? errorBg : null);
+ }
+ }
+
+ /** Anti-flicker: only push cell text that actually changed. */
+ private static void applyRowTextsIfChanged(TableItem item, List<String>
texts) {
+ for (int col = 0; col < texts.size(); col++) {
+ String next = Const.NVL(texts.get(col), "");
+ if (!next.equals(item.getText(col))) {
+ item.setText(col, next);
}
}
}
@@ -1247,9 +1378,16 @@ public class HopGuiPipelineGridDelegate {
return;
}
Table table = pipelineGridView.table;
- if (gridSortColumn >= 0 && gridSortColumn < table.getColumnCount()) {
- table.setSortColumn(table.getColumn(gridSortColumn));
- table.setSortDirection(gridSortDescending ? SWT.DOWN : SWT.UP);
+ if (gridSortColumn < 0 || gridSortColumn >= table.getColumnCount()) {
+ return;
+ }
+ TableColumn wanted = table.getColumn(gridSortColumn);
+ int direction = gridSortDescending ? SWT.DOWN : SWT.UP;
+ if (table.getSortColumn() != wanted) {
+ table.setSortColumn(wanted);
+ }
+ if (table.getSortDirection() != direction) {
+ table.setSortDirection(direction);
}
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
index a69c6f5483..4bfdca989c 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
@@ -115,6 +115,7 @@ public class ConfigGuiOptionsTab {
private Text wMaxPreviewCellLength;
private Button wShowPreviewLineBreaks;
private Button wMetricsPanelShowUnits;
+ private Button wMetricsPanelDynamicColumnResize;
private Button wMetricsPanelShowInput;
private Button wMetricsPanelShowRead;
private Button wMetricsPanelShowOutput;
@@ -215,6 +216,10 @@ public class ConfigGuiOptionsTab {
}
if (wMetricsPanelShowUnits != null &&
!wMetricsPanelShowUnits.isDisposed()) {
wMetricsPanelShowUnits.setSelection(props.isMetricsPanelShowUnits());
+ if (wMetricsPanelDynamicColumnResize != null
+ && !wMetricsPanelDynamicColumnResize.isDisposed()) {
+
wMetricsPanelDynamicColumnResize.setSelection(props.isMetricsPanelDynamicColumnResize());
+ }
wMetricsPanelShowInput.setSelection(props.isMetricsPanelShowInput());
wMetricsPanelShowRead.setSelection(props.isMetricsPanelShowRead());
wMetricsPanelShowOutput.setSelection(props.isMetricsPanelShowOutput());
@@ -1024,6 +1029,16 @@ public class ConfigGuiOptionsTab {
margin);
lastMetricsPanelControl = wMetricsPanelShowUnits;
+ wMetricsPanelDynamicColumnResize =
+ createCheckbox(
+ metricsPanelContent,
+ "EnterOptionsDialog.MetricsPanel.DynamicColumnResize.Label",
+ "EnterOptionsDialog.MetricsPanel.DynamicColumnResize.ToolTip",
+ props.isMetricsPanelDynamicColumnResize(),
+ lastMetricsPanelControl,
+ margin);
+ lastMetricsPanelControl = wMetricsPanelDynamicColumnResize;
+
wMetricsPanelShowInput =
createCheckbox(
metricsPanelContent,
@@ -1336,6 +1351,10 @@ public class ConfigGuiOptionsTab {
Const.toInt(wMaxPreviewCellLength.getText(),
props.getMaxPreviewCellLength()));
props.setShowPreviewLineBreaksAsSymbols(wShowPreviewLineBreaks.getSelection());
props.setMetricsPanelShowUnits(wMetricsPanelShowUnits.getSelection());
+ if (wMetricsPanelDynamicColumnResize != null
+ && !wMetricsPanelDynamicColumnResize.isDisposed()) {
+
props.setMetricsPanelDynamicColumnResize(wMetricsPanelDynamicColumnResize.getSelection());
+ }
props.setMetricsPanelShowInput(wMetricsPanelShowInput.getSelection());
props.setMetricsPanelShowRead(wMetricsPanelShowRead.getSelection());
props.setMetricsPanelShowOutput(wMetricsPanelShowOutput.getSelection());
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 45936bfa78..addd19e1b8 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
@@ -110,6 +110,8 @@ EnterOptionsDialog.AutoLayout.MoveNotes.ToolTip=When
arranging, move each note a
EnterOptionsDialog.Section.MetricsPanel=Metrics panel
EnterOptionsDialog.MetricsPanel.ShowUnits.Label=Show units in cells
EnterOptionsDialog.MetricsPanel.ShowUnits.ToolTip=When enabled, units (e.g. r,
h:m:s, rows/s) are shown in the pipeline metrics grid cells.
+EnterOptionsDialog.MetricsPanel.DynamicColumnResize.Label=Dynamic column
resizing
+EnterOptionsDialog.MetricsPanel.DynamicColumnResize.ToolTip=When enabled,
metrics columns grow during execution as values get wider. Turn this off to
keep the widths you set. Use View > Fit columns to content to pack once.
EnterOptionsDialog.MetricsPanel.ShowInput.Label=Show Input
EnterOptionsDialog.MetricsPanel.ShowRead.Label=Show Read
EnterOptionsDialog.MetricsPanel.ShowOutput.Label=Show Output
diff --git
a/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
index 30c6e7ab85..8d474ca9e9 100644
---
a/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
@@ -256,8 +256,12 @@ PipelineLog.Button.ShowOnlyActiveTransforms=Hide inactive
PipelineLog.Button.ShowOnlySelectedTransforms=Show only selected transforms
PipelineLog.Search.TransformName.Placeholder=Filter by transform name...
PipelineLog.MetricsView.View=View
-PipelineLog.MetricsView.View.Tooltip=Show or hide columns and units in the
metrics grid
+PipelineLog.MetricsView.View.Tooltip=Show or hide columns, units, and column
resizing in the metrics grid
PipelineLog.MetricsView.ShowUnits=Show units in cells
+PipelineLog.MetricsView.DynamicColumnResize=Dynamic column resizing
+PipelineLog.MetricsView.DynamicColumnResize.Tooltip=When enabled, auto-sized
columns grow during execution as values get wider. Turn this off to keep the
widths you set.
+PipelineLog.MetricsView.FitColumns=Fit columns to content
+PipelineLog.MetricsView.FitColumns.Tooltip=Pack columns once to the current
cell and header text. Columns you resized by dragging are left as they are.
PipelineLog.MetricsView.ShowInput=Show Input
PipelineLog.MetricsView.ShowRead=Show Read
PipelineLog.MetricsView.ShowOutput=Show Output