diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/dialog/AboutDialog.java b/ui/src/main/java/org/apache/hop/ui/hopgui/dialog/AboutDialog.java index 1fbcf65e718..7ffab0eb35b 100644 --- a/ui/src/main/java/org/apache/hop/ui/hopgui/dialog/AboutDialog.java +++ b/ui/src/main/java/org/apache/hop/ui/hopgui/dialog/AboutDialog.java @@ -17,13 +17,19 @@ package org.apache.hop.ui.hopgui.dialog; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; +import java.util.TreeSet; +import org.apache.hop.core.Const; import org.apache.hop.core.HopVersionProvider; import org.apache.hop.core.variables.IVariables; import org.apache.hop.core.variables.VariableRegistry; import org.apache.hop.core.variables.VariableScope; import org.apache.hop.i18n.BaseMessages; import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; import org.apache.hop.ui.core.dialog.ErrorDialog; import org.apache.hop.ui.core.gui.GuiResource; import org.apache.hop.ui.hopgui.HopGui; @@ -42,15 +48,21 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.ExpandBar; +import org.eclipse.swt.widgets.ExpandItem; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; -/** A dialog to display version information. */ +/** A dialog to display version and system information. */ public class AboutDialog extends Dialog { private static final Class PKG = AboutDialog.class; + private static final String HOP_URL = "https://hop.apache.org"; + private static final long MB = 1024L * 1024L; + + /** Full set of Java properties shown, sorted, in the collapsible "Advanced" section. */ private static final String[] JAVA_PROPERTIES = new String[] { "os.name", @@ -64,6 +76,10 @@ public class AboutDialog extends Dialog { }; private Shell shell; + private ExpandBar expandBar; + private ExpandItem advanced; + private Button wOk; + private int collapsedHeight; public AboutDialog(Shell parent) { super(parent, SWT.NONE); @@ -72,83 +88,153 @@ public AboutDialog(Shell parent) { public void open() { Shell parent = getParent(); Display display = parent.getDisplay(); - - shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.SHEET | SWT.RESIZE); + shell = new Shell(parent, BaseDialog.getDefaultDialogStyle()); shell.setText(BaseMessages.getString(PKG, "AboutDialog.Title")); shell.setImage(GuiResource.getInstance().getImageHopUi()); - shell.setSize(700, 500); - shell.setMinimumSize(450, 300); + int margin = PropsUi.getMargin(); FormLayout formLayout = new FormLayout(); formLayout.marginWidth = PropsUi.getFormMargin(); formLayout.marginHeight = PropsUi.getFormMargin(); shell.setLayout(formLayout); PropsUi.setLook(shell); - // Composite for logo, app name & version, and centering link - Composite composite = new Composite(shell, SWT.NONE); - FormData fdLink = new FormData(); - fdLink.top = new FormAttachment(0, 0); - fdLink.left = new FormAttachment(0, 0); - fdLink.right = new FormAttachment(100, 0); - composite.setLayoutData(fdLink); - GridLayout gridLayout = new GridLayout(2, false); - gridLayout.marginWidth = 0; - composite.setLayout(gridLayout); - PropsUi.setLook(composite); - - // Widget application logo - Label wLogo = new Label(composite, SWT.CENTER); + // ----- Header: logo on the left, name / version / link stacked on the right ----- + Composite header = new Composite(shell, SWT.NONE); + PropsUi.setLook(header); + GridLayout headerLayout = new GridLayout(2, false); + headerLayout.marginWidth = 0; + headerLayout.marginHeight = 0; + headerLayout.horizontalSpacing = 2 * margin; + header.setLayout(headerLayout); + FormData fdHeader = new FormData(); + fdHeader.top = new FormAttachment(0, 0); + fdHeader.left = new FormAttachment(0, 0); + fdHeader.right = new FormAttachment(100, 0); + header.setLayoutData(fdHeader); + + Label wLogo = new Label(header, SWT.NONE); wLogo.setImage( SwtSvgImageUtil.getImageAsResource(display, "ui/images/logo_hop.svg") - .getAsBitmapForSize(display, 100, 100)); - wLogo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 3)); + .getAsBitmapForSize(display, 90, 90)); + wLogo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + Composite titleBox = new Composite(header, SWT.NONE); + PropsUi.setLook(titleBox); + GridLayout titleLayout = new GridLayout(1, false); + titleLayout.marginWidth = 0; + titleLayout.marginHeight = 0; + titleBox.setLayout(titleLayout); + titleBox.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - // Widget application name - Label wName = new Label(composite, SWT.CENTER); + Label wName = new Label(titleBox, SWT.LEFT); wName.setText("Apache Hop"); wName.setFont(GuiResource.getInstance().getFontBold()); - wName.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + wName.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false)); PropsUi.setLook(wName); - // Widget application version - Text wVersion = new Text(composite, SWT.READ_ONLY | SWT.CENTER); - wVersion.setText(this.getVersion()); - wVersion.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + Label wVersion = new Label(titleBox, SWT.LEFT); + wVersion.setText(getVersionLabel()); + wVersion.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false)); PropsUi.setLook(wVersion); - Link wLink = new Link(composite, SWT.WRAP | SWT.MULTI); - wLink.setText("hop.apache.org"); - wLink.addListener( - SWT.Selection, - e -> { - try { - EnvironmentUtils.getInstance().openUrl("https://hop.apache.org"); - } catch (Exception ex) { - new ErrorDialog(shell, "Error", "Error opening URL", ex); - } - }); - wLink.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + Link wLink = new Link(titleBox, SWT.NONE); + wLink.setText("hop.apache.org"); + wLink.addListener(SWT.Selection, e -> openUrl()); + wLink.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false)); PropsUi.setLook(wLink); - // Buttons - Button wOk = new Button(shell, SWT.PUSH); + // ----- Separator ----- + Label separator = new Label(shell, SWT.SEPARATOR | SWT.HORIZONTAL); + FormData fdSeparator = new FormData(); + fdSeparator.top = new FormAttachment(header, 2 * margin); + fdSeparator.left = new FormAttachment(0, 0); + fdSeparator.right = new FormAttachment(100, 0); + separator.setLayoutData(fdSeparator); + + // ----- "System information" heading with a Copy button on the right ----- + Button wCopy = new Button(shell, SWT.PUSH); + wCopy.setText(BaseMessages.getString(PKG, "AboutDialog.Button.Copy")); + wCopy.setToolTipText(BaseMessages.getString(PKG, "AboutDialog.Button.Copy.Tooltip")); + wCopy.addListener(SWT.Selection, e -> copyToClipboard(wCopy)); + FormData fdCopy = new FormData(); + fdCopy.top = new FormAttachment(separator, margin); + fdCopy.right = new FormAttachment(100, 0); + wCopy.setLayoutData(fdCopy); + + Label wSysInfo = new Label(shell, SWT.LEFT); + wSysInfo.setText(BaseMessages.getString(PKG, "AboutDialog.SystemInformation")); + wSysInfo.setFont(GuiResource.getInstance().getFontBold()); + PropsUi.setLook(wSysInfo); + FormData fdSysInfo = new FormData(); + fdSysInfo.top = new FormAttachment(wCopy, 0, SWT.CENTER); + fdSysInfo.left = new FormAttachment(0, 0); + wSysInfo.setLayoutData(fdSysInfo); + + // ----- The essentials: a compact aligned key / value block ----- + Composite info = new Composite(shell, SWT.NONE); + PropsUi.setLook(info); + GridLayout infoLayout = new GridLayout(2, false); + infoLayout.marginWidth = 0; + infoLayout.marginHeight = 0; + infoLayout.horizontalSpacing = 3 * margin; + infoLayout.verticalSpacing = Math.max(2, margin / 2); + info.setLayout(infoLayout); + FormData fdInfo = new FormData(); + fdInfo.top = new FormAttachment(wCopy, margin); + fdInfo.left = new FormAttachment(0, 0); + fdInfo.right = new FormAttachment(100, 0); + info.setLayoutData(fdInfo); + + for (String[] row : getEssentials()) { + Label wKey = new Label(info, SWT.LEFT); + wKey.setText(row[0]); + wKey.setFont(GuiResource.getInstance().getFontBold()); + GridData gdKey = new GridData(SWT.LEFT, SWT.TOP, false, false); + gdKey.widthHint = (int) (140 * PropsUi.getInstance().getZoomFactor()); + wKey.setLayoutData(gdKey); + PropsUi.setLook(wKey); + + Label wValue = new Label(info, SWT.LEFT); + wValue.setText(row[1]); + wValue.setToolTipText(row[1]); + wValue.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + PropsUi.setLook(wValue); + } + + // ----- OK button, anchored at the bottom of the (resizable) dialog ----- + wOk = new Button(shell, SWT.PUSH); wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); wOk.addListener(SWT.Selection, e -> ok()); - BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOk}, PropsUi.getMargin(), null); + BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOk}, margin, null); + + // ----- Collapsible "Advanced" section with the full property / classpath dump ----- + expandBar = new ExpandBar(shell, SWT.V_SCROLL); + PropsUi.setLook(expandBar); + FormData fdBar = new FormData(); + fdBar.top = new FormAttachment(info, 2 * margin); + fdBar.left = new FormAttachment(0, 0); + fdBar.right = new FormAttachment(100, 0); + fdBar.bottom = new FormAttachment(wOk, -margin); + expandBar.setLayoutData(fdBar); - // Widget system properties - Text wText = + Text wProperties = new Text( - shell, SWT.READ_ONLY | SWT.WRAP | SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); - wText.setText(getProperties()); - FormData fdText = new FormData(); - fdText.top = new FormAttachment(composite, PropsUi.getMargin()); - fdText.left = new FormAttachment(0, 0); - fdText.right = new FormAttachment(100, 0); - fdText.bottom = new FormAttachment(wOk, -PropsUi.getMargin()); - wText.setLayoutData(fdText); - PropsUi.setLook(wText); + expandBar, + SWT.READ_ONLY | SWT.WRAP | SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); + wProperties.setText(getProperties()); + PropsUi.setLook(wProperties); + + advanced = new ExpandItem(expandBar, SWT.NONE); + advanced.setText(BaseMessages.getString(PKG, "AboutDialog.Advanced")); + advanced.setControl(wProperties); + advanced.setHeight((int) (240 * PropsUi.getInstance().getZoomFactor())); + advanced.setExpanded(false); + + // Keep the properties view filling the available height and grow / shrink on toggle. + shell.addListener(SWT.Resize, e -> fillAdvanced()); + expandBar.addListener(SWT.Expand, e -> display.asyncExec(this::onExpand)); + expandBar.addListener(SWT.Collapse, e -> display.asyncExec(this::onCollapse)); // Detect [X] or ALT-F4 or something that kills this window... shell.addShellListener( @@ -159,6 +245,17 @@ public void shellClosed(ShellEvent e) { } }); + // Size the dialog: compact by default, with the Advanced section collapsed. + double zoom = PropsUi.getInstance().getZoomFactor(); + int width = + Math.clamp( + shell.computeSize(SWT.DEFAULT, SWT.DEFAULT).x, (int) (520 * zoom), (int) (780 * zoom)); + shell.setSize(width, (int) (600 * zoom)); + shell.layout(true, true); + collapsedHeight = computeCollapsedHeight(margin); + shell.setSize(width, collapsedHeight); + shell.setMinimumSize(width, collapsedHeight); + shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { @@ -167,26 +264,161 @@ public void shellClosed(ShellEvent e) { } } + /** Compact height with the Advanced section collapsed, including the window trim. */ + private int computeCollapsedHeight(int margin) { + int clientHeight = + expandBar.getLocation().y + + advanced.getHeaderHeight() + + 2 * expandBar.getSpacing() + + margin + + wOk.getSize().y + + PropsUi.getFormMargin(); + return clientHeight + (shell.getSize().y - shell.getClientArea().height); + } + + private void onExpand() { + if (shell.isDisposed()) { + return; + } + int grow = (int) (260 * PropsUi.getInstance().getZoomFactor()); + shell.setSize(shell.getSize().x, Math.max(shell.getSize().y, collapsedHeight + grow)); + shell.layout(true, true); + fillAdvanced(); + } + + private void onCollapse() { + if (shell.isDisposed()) { + return; + } + shell.setSize(shell.getSize().x, collapsedHeight); + shell.layout(true, true); + } + + /** When expanded, size the properties view to fill the space above the OK button. */ + private void fillAdvanced() { + if (shell == null || shell.isDisposed() || advanced == null || !advanced.getExpanded()) { + return; + } + int height = + expandBar.getClientArea().height - advanced.getHeaderHeight() - 2 * expandBar.getSpacing(); + int minHeight = (int) (60 * PropsUi.getInstance().getZoomFactor()); + if (height < minHeight) { + height = minHeight; + } + if (height != advanced.getHeight()) { + advanced.setHeight(height); + } + } + + /** Copy the full system report to the clipboard and briefly acknowledge on the button. */ + private void copyToClipboard(Button button) { + GuiResource.getInstance().toClipboard(getReport()); + String original = button.getText(); + button.setText(BaseMessages.getString(PKG, "AboutDialog.Button.Copied")); + button + .getDisplay() + .timerExec( + 1500, + () -> { + if (!button.isDisposed()) { + button.setText(original); + } + }); + } + + private void openUrl() { + try { + EnvironmentUtils.getInstance().openUrl(HOP_URL); + } catch (Exception ex) { + new ErrorDialog(shell, "Error", "Error opening URL", ex); + } + } + protected String getVersion() { HopVersionProvider versionProvider = new HopVersionProvider(); return versionProvider.getVersion()[0]; } - private String getProperties() { - Set names = VariableRegistry.getInstance().getVariableNames(VariableScope.SYSTEM); - for (String name : JAVA_PROPERTIES) { - names.add(name); + private String getVersionLabel() { + String version = getVersion(); + if (version == null || version.isBlank()) { + return BaseMessages.getString(PKG, "AboutDialog.DevelopmentBuild"); + } + return version; + } + + /** The handful of values most people open this dialog for, shown at the top. */ + private List getEssentials() { + IVariables variables = HopGui.getInstance().getVariables(); + List rows = new ArrayList<>(); + rows.add(essential("HopVersion", getVersionLabel())); + rows.add( + essential( + "OperatingSystem", + prop(variables, "os.name") + + " " + + prop(variables, "os.version") + + " (" + + prop(variables, "os.arch") + + ")")); + rows.add( + essential( + "Java", + prop(variables, "java.version") + " (" + prop(variables, "java.vm.vendor") + ")")); + rows.add(essential("JavaSpecification", prop(variables, "java.specification.version"))); + rows.add(essential("FileEncoding", prop(variables, "file.encoding"))); + rows.add(essential("Memory", getMemory())); + rows.add(essential("Processors", Integer.toString(Runtime.getRuntime().availableProcessors()))); + rows.add(essential("ConfigFolder", Const.HOP_CONFIG_FOLDER)); + return rows; + } + + private String[] essential(String key, String value) { + return new String[] { + BaseMessages.getString(PKG, "AboutDialog.Label." + key), value == null ? "" : value + }; + } + + private String getMemory() { + Runtime runtime = Runtime.getRuntime(); + long usedMb = (runtime.totalMemory() - runtime.freeMemory()) / MB; + long max = runtime.maxMemory(); + String maxLabel = + max == Long.MAX_VALUE + ? BaseMessages.getString(PKG, "AboutDialog.Memory.NoLimit") + : (max / MB) + " MB"; + return usedMb + " MB / " + maxLabel; + } + + private String prop(IVariables variables, String name) { + String value = variables.getVariable(name); + if (value == null || value.isEmpty()) { + value = System.getProperty(name, ""); } + return value; + } + + /** The complete, sorted list of system properties shown in the Advanced section. */ + private String getProperties() { + Set names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + names.addAll(VariableRegistry.getInstance().getVariableNames(VariableScope.SYSTEM)); + Collections.addAll(names, JAVA_PROPERTIES); IVariables variables = HopGui.getInstance().getVariables(); StringBuilder builder = new StringBuilder(); for (String name : names) { - builder.append(name); - builder.append('='); - builder.append(variables.getVariable(name, "")); - builder.append('\n'); + builder.append(name).append('=').append(variables.getVariable(name, "")).append('\n'); } + return builder.toString(); + } + /** Essentials followed by the full property dump — what the Copy button puts on the clipboard. */ + private String getReport() { + StringBuilder builder = new StringBuilder(); + for (String[] row : getEssentials()) { + builder.append(row[0]).append(": ").append(row[1]).append('\n'); + } + builder.append('\n').append(getProperties()); return builder.toString(); } diff --git a/ui/src/main/resources/org/apache/hop/ui/hopgui/dialog/messages/messages_en_US.properties b/ui/src/main/resources/org/apache/hop/ui/hopgui/dialog/messages/messages_en_US.properties index 522a5358105..edafd0e1072 100644 --- a/ui/src/main/resources/org/apache/hop/ui/hopgui/dialog/messages/messages_en_US.properties +++ b/ui/src/main/resources/org/apache/hop/ui/hopgui/dialog/messages/messages_en_US.properties @@ -16,6 +16,21 @@ # limitations under the License. # +AboutDialog.Advanced=Advanced - all system properties and classpath +AboutDialog.Button.Copied=Copied +AboutDialog.Button.Copy=Copy +AboutDialog.Button.Copy.Tooltip=Copy the system information to the clipboard +AboutDialog.DevelopmentBuild=development build +AboutDialog.Label.ConfigFolder=Configuration folder +AboutDialog.Label.FileEncoding=File encoding +AboutDialog.Label.HopVersion=Hop version +AboutDialog.Label.Java=Java +AboutDialog.Label.JavaSpecification=Java specification +AboutDialog.Label.Memory=Memory +AboutDialog.Label.OperatingSystem=Operating system +AboutDialog.Label.Processors=Processors +AboutDialog.Memory.NoLimit=no limit +AboutDialog.SystemInformation=System information AboutDialog.Title=About AnalyseImpactProgressDialog.Dialog.UnableToAnalyzeImpact.Messages=Unable to analyze database impact because of an error\: AnalyseImpactProgressDialog.Dialog.UnableToAnalyzeImpact.Title=Error checking pipeline