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 fd32042019 Issue #8098 : Allow help files to be opened in a dialog 
window (#8100)
fd32042019 is described below

commit fd3204201934ebf7ee8f31dbe462534778892849
Author: Matt Casters <[email protected]>
AuthorDate: Wed Aug 26 14:30:18 2026 +0200

    Issue #8098 : Allow help files to be opened in a dialog window (#8100)
    
    Add a third destination for Hop GUI help (external browser, Explorer tab,
    or a modeless dialog on top of the current context). Route Help buttons
    and F1 through the same setting, migrate the old openingHelpFiles checkbox,
    and document the option with a screenshot.
---
 ...onfiguration-perspective-open-help-pages-in.png | Bin 0 -> 10762 bytes
 .../pages/hop-gui/perspective-configuration.adoc   |   3 +
 .../pages/hop-gui/perspective-file-explorer.adoc   |   3 +
 .../apache/hop/ui/core/dialog/ShowHelpDialog.java  | 172 +++++++++++++++------
 .../main/java/org/apache/hop/ui/hopgui/HopGui.java |   7 +-
 .../explorer/config/ExplorerPerspectiveConfig.java |  68 ++++----
 .../config/ExplorerPerspectiveConfigPlugin.java    |  77 +++++++--
 .../java/org/apache/hop/ui/util/HelpOpenMode.java  |  85 ++++++++++
 .../java/org/apache/hop/ui/util/HelpUtils.java     | 104 ++++++++++---
 .../ui/hopgui/messages/messages_en_US.properties   |   1 +
 .../config/messages/messages_en_US.properties      |   4 +-
 .../config/messages/messages_fr_FR.properties      |   4 +-
 .../config/messages/messages_pt_BR.properties      |   4 +-
 .../hop/ui/util/messages/messages_en_US.properties |  20 +++
 .../hop/ui/util/messages/messages_fr_FR.properties |  20 +++
 .../hop/ui/util/messages/messages_pt_BR.properties |  20 +++
 .../hop/ui/core/dialog/ShowHelpDialogTest.java     |  35 +++++
 .../config/ExplorerPerspectiveConfigTest.java      |  87 +++++++++++
 .../org/apache/hop/ui/util/HelpOpenModeTest.java   |  52 +++++++
 .../java/org/apache/hop/ui/util/HelpUtilsTest.java |  49 ++++++
 20 files changed, 688 insertions(+), 127 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/configuration-perspective-open-help-pages-in.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/configuration-perspective-open-help-pages-in.png
new file mode 100644
index 0000000000..6bd33d6cff
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/configuration-perspective-open-help-pages-in.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
index 8cf2194d54..b9c2504fe6 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
@@ -131,6 +131,9 @@ The following plugins are available by default:
   ** The initial depth to load not lazily: Controls how many folder levels are 
loaded immediately when opening folders.
   ** The maximum file size to load: Sets the maximum file size (in MB) that 
will be loaded when opening files.
   ** Show file explorer panel by default: When enabled, the file explorer 
panel (project tree) is shown by default when opening the explorer perspective. 
When disabled, the panel starts hidden.
+  ** Open help pages in: Where Hop GUI opens documentation from Help buttons 
and F1. *External browser* (default) uses the system browser. *Hop GUI tab* 
opens an HTML tab in the File Explorer perspective. *Dialog window* opens a 
modeless help window on top of the current dialog or main window so you can 
keep editing.
++
+image::hop-gui/configuration-perspective-open-help-pages-in.png[Open help 
pages in,width="90%"]
 * xref:technology/google/index.adoc[Google Cloud] configuration options 
(service account JSON key file).
 * xref:vfs/google-drive-vfs.adoc#_configuration[Google Drive] VFS 
configuration options.
 * xref:projects/index.adoc[Project] configuration options
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-file-explorer.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-file-explorer.adoc
index 9887829321..feaa000341 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-file-explorer.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-file-explorer.adoc
@@ -64,6 +64,9 @@ The File Explorer perspective can be configured through the 
xref:hop-gui/perspec
 * **The initial depth to load not lazily**: Controls how many folder levels 
are loaded immediately when opening folders in the file explorer tree.
 * **The maximum file size to load**: Sets the maximum file size (in MB) that 
will be loaded when opening files in the explorer.
 * **Show file explorer panel by default**: When enabled, the file explorer 
panel (project tree) is shown by default when opening the explorer perspective. 
When disabled, the panel starts hidden, giving you more workspace for editing 
files.
+* **Open help pages in**: Where Help buttons and F1 open documentation: the 
system browser, a tab in this perspective, or a dialog on top of the current 
window.
++
+image::hop-gui/configuration-perspective-open-help-pages-in.png[Open help 
pages in,width="90%"]
 
 == Git integration
 
diff --git a/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowHelpDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowHelpDialog.java
index 3f6f7eff0a..ee0ca6747b 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowHelpDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowHelpDialog.java
@@ -17,14 +17,14 @@
 
 package org.apache.hop.ui.core.dialog;
 
-import java.net.MalformedURLException;
-import java.net.URL;
 import org.apache.hop.core.Const;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
-import org.apache.hop.laf.BasePropertyHandler;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.WindowProperty;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.apache.hop.ui.util.EnvironmentUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.browser.Browser;
@@ -44,21 +44,23 @@ import org.eclipse.swt.widgets.Text;
 import org.eclipse.swt.widgets.ToolBar;
 import org.eclipse.swt.widgets.ToolItem;
 
+/**
+ * Modeless documentation viewer parented to the current context shell 
(transform/action dialog or
+ * the main Hop GUI window).
+ */
 public class ShowHelpDialog extends Dialog {
+  public static final String SHELL_DATA_KEY = "hop.help.dialog";
+
   private static final Class<?> PKG = HopGui.class;
 
-  private static final String DOC_URL =
-      Const.getDocUrl(BasePropertyHandler.getProperty("documentationUrl"));
-  private static final String PREFIX = "https://help";;
-  private static final String PRINT_PREFIX = "https://f1.help";;
   private static final String PRINT_SCRIPT = "javascript:window.print();";
   private static final int MARGIN = 5;
+  private static final int DEFAULT_WIDTH = 900;
+  private static final int DEFAULT_HEIGHT = 700;
 
-  private boolean fromPrint;
-
-  private String dialogTitle;
+  private final String dialogTitle;
   private String url;
-  private String homeURL;
+  private final String homeURL;
 
   private Browser wBrowser;
 
@@ -69,22 +71,29 @@ public class ShowHelpDialog extends Dialog {
 
   private Shell shell;
 
-  public ShowHelpDialog(Shell parent, String dialogTitle, String url, String 
header) {
-    super(parent, SWT.NONE);
-    this.dialogTitle = BaseMessages.getString(PKG, 
"HopGui.Documentation.Hop.Title");
-    this.url = url;
-    try {
-      this.homeURL = new URL(DOC_URL).toString();
-    } catch (MalformedURLException e) {
-    }
+  public ShowHelpDialog(Shell parent, String url) {
+    this(parent, BaseMessages.getString(PKG, 
"HopGui.Documentation.Hop.Title"), url);
   }
 
   public ShowHelpDialog(Shell parent, String dialogTitle, String url) {
-    this(parent, dialogTitle, url, "");
+    super(parent, SWT.NONE);
+    this.dialogTitle =
+        Utils.isEmpty(dialogTitle)
+            ? BaseMessages.getString(PKG, "HopGui.Documentation.Hop.Title")
+            : dialogTitle;
+    this.url = url;
+    this.homeURL = Const.getDocUrl("");
   }
 
   protected Shell createShell(Shell parent) {
-    return new Shell(parent, BaseDialog.getDefaultDialogStyle());
+    return new Shell(parent, helpDialogStyle());
+  }
+
+  static int helpDialogStyle() {
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      return SWT.DIALOG_TRIM | SWT.RESIZE;
+    }
+    return SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | SWT.MIN;
   }
 
   public void open() {
@@ -93,12 +102,26 @@ public class ShowHelpDialog extends Dialog {
     PropsUi props = PropsUi.getInstance();
 
     shell = createShell(parent);
-    shell.setImage(GuiResource.getInstance().getImageHopUi());
+    shell.setImage(GuiResource.getInstance().getImageHelp());
     shell.setLayout(new FormLayout());
     shell.setText(dialogTitle);
     PropsUi.setLook(shell);
 
     Cursor cursorHand = new Cursor(display, SWT.CURSOR_HAND);
+    Color urlColor = new Color(display, props.contrastColor(101, 101, 101));
+    shell.addListener(SWT.Close, e -> PropsUi.getInstance().setScreen(new 
WindowProperty(shell)));
+    shell.addDisposeListener(
+        e -> {
+          if (cursorHand != null && !cursorHand.isDisposed()) {
+            cursorHand.dispose();
+          }
+          if (urlColor != null && !urlColor.isDisposed()) {
+            urlColor.dispose();
+          }
+          if (parent != null && !parent.isDisposed() && 
parent.getData(SHELL_DATA_KEY) == this) {
+            parent.setData(SHELL_DATA_KEY, null);
+          }
+        });
 
     ToolBar navigateToolBar = new ToolBar(shell, SWT.FLAT);
     FormData fdtoolBarBack = new FormData();
@@ -149,16 +172,35 @@ public class ShowHelpDialog extends Dialog {
     tltmPrint.setEnabled(true);
     tltmPrint.addListener(SWT.Selection, e -> print());
 
+    ToolItem tltmExternal = new ToolItem(printToolBar, SWT.NONE);
+    
tltmExternal.setImage(GuiResource.getInstance().getImage("ui/images/html.svg"));
+    tltmExternal.setToolTipText(
+        BaseMessages.getString(PKG, 
"HopGui.Documentation.Tooltip.OpenExternal"));
+    tltmExternal.addListener(SWT.Selection, e -> openExternal());
+
     textURL = new Text(shell, SWT.BORDER);
     FormData fdtext = new FormData();
     fdtext.top = new FormAttachment(0, MARGIN);
     fdtext.right = new FormAttachment(printToolBar, -MARGIN);
     fdtext.left = new FormAttachment(navigateToolBar, MARGIN);
     textURL.setLayoutData(fdtext);
-    textURL.setForeground(new Color(display, props.contrastColor(101, 101, 
101)));
+    textURL.setForeground(urlColor);
+    textURL.setText(Const.NVL(url, ""));
+    textURL.addListener(
+        SWT.DefaultSelection,
+        e -> {
+          String location = textURL.getText();
+          if (!Utils.isEmpty(location) && wBrowser != null && 
!wBrowser.isDisposed()) {
+            wBrowser.setUrl(location);
+          }
+        });
 
-    // Browser
-    wBrowser = new Browser(shell, SWT.NONE);
+    try {
+      wBrowser = new Browser(shell, SWT.NONE);
+    } catch (RuntimeException e) {
+      dispose();
+      throw e;
+    }
     FormData fdBrowser = new FormData();
     fdBrowser.top = new FormAttachment(textURL, MARGIN);
     fdBrowser.right = new FormAttachment(100, 0);
@@ -170,9 +212,18 @@ public class ShowHelpDialog extends Dialog {
 
     addProgressAndLocationListener();
 
-    textURL.setFocus();
+    shell.addListener(
+        SWT.Traverse,
+        e -> {
+          if (e.detail == SWT.TRAVERSE_ESCAPE) {
+            e.doit = false;
+            dispose();
+          }
+        });
 
-    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> ok());
+    BaseTransformDialog.setSize(shell, DEFAULT_WIDTH, DEFAULT_HEIGHT);
+    textURL.setFocus();
+    shell.open();
   }
 
   private void addProgressAndLocationListener() {
@@ -185,11 +236,7 @@ public class ShowHelpDialog extends Dialog {
 
           @Override
           public void completed(ProgressEvent event) {
-            if (fromPrint) {
-              wBrowser.execute(PRINT_SCRIPT);
-              fromPrint = false;
-            }
-            if (!EnvironmentUtils.getInstance().isWeb()) {
+            if (!EnvironmentUtils.getInstance().isWeb() && tltmBack != null) {
               // Browser in RAP does not implement back() and forward()
               setBackEnable(wBrowser.isBackEnabled());
               setForwardEnable(wBrowser.isForwardEnabled());
@@ -201,7 +248,7 @@ public class ShowHelpDialog extends Dialog {
         new LocationListener() {
           @Override
           public void changing(LocationEvent event) {
-            if (event.location.endsWith(".pdf")) {
+            if (event.location != null && event.location.endsWith(".pdf")) {
               try {
                 EnvironmentUtils.getInstance().openUrl(event.location);
               } catch (Exception e) {
@@ -213,7 +260,9 @@ public class ShowHelpDialog extends Dialog {
 
           @Override
           public void changed(LocationEvent event) {
-            textURL.setText(event.location);
+            if (event.location != null && textURL != null && 
!textURL.isDisposed()) {
+              textURL.setText(event.location);
+            }
           }
         };
     wBrowser.addProgressListener(progressListener);
@@ -237,29 +286,58 @@ public class ShowHelpDialog extends Dialog {
   }
 
   private void print() {
-    String printURL = wBrowser.getUrl();
-    if (printURL.startsWith(PREFIX)) {
-      printURL = printURL.replace(PREFIX, PRINT_PREFIX);
-      fromPrint = true;
-      wBrowser.setUrl(printURL);
-    } else {
-      wBrowser.execute(PRINT_SCRIPT);
+    wBrowser.execute(PRINT_SCRIPT);
+  }
+
+  private void openExternal() {
+    String location = wBrowser.getUrl();
+    if (Utils.isEmpty(location)) {
+      location = url;
+    }
+    try {
+      EnvironmentUtils.getInstance().openUrl(location);
+    } catch (Exception e) {
+      new ErrorDialog(shell, "Error", "Error opening URL", e);
     }
   }
 
   private void setBackEnable(boolean enable) {
-    tltmBack.setEnabled(enable);
+    if (tltmBack != null && !tltmBack.isDisposed()) {
+      tltmBack.setEnabled(enable);
+    }
   }
 
   private void setForwardEnable(boolean enable) {
-    tltmForward.setEnabled(enable);
+    if (tltmForward != null && !tltmForward.isDisposed()) {
+      tltmForward.setEnabled(enable);
+    }
   }
 
-  public void dispose() {
-    shell.dispose();
+  public void setUrl(String url) {
+    this.url = url;
+    if (wBrowser != null && !wBrowser.isDisposed()) {
+      wBrowser.setUrl(url);
+    }
+    if (textURL != null && !textURL.isDisposed() && url != null) {
+      textURL.setText(url);
+    }
+  }
+
+  public boolean isDisposed() {
+    return shell == null || shell.isDisposed();
   }
 
-  private void ok() {
-    dispose();
+  public void forceActive() {
+    if (shell != null && !shell.isDisposed()) {
+      shell.setMinimized(false);
+      shell.setActive();
+      shell.forceActive();
+    }
+  }
+
+  public void dispose() {
+    if (shell != null && !shell.isDisposed()) {
+      shell.dispose();
+    }
   }
 }
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 4aa4225097..4d37882f2e 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -135,6 +135,7 @@ import 
org.apache.hop.ui.hopgui.search.SearchEverywhereDialog;
 import org.apache.hop.ui.hopgui.welcome.WelcomeDialog;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.apache.hop.ui.util.EnvironmentUtils;
+import org.apache.hop.ui.util.HelpUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.StackLayout;
 import org.eclipse.swt.events.ShellAdapter;
@@ -2576,11 +2577,7 @@ public class HopGui
     HopPerspectivePlugin plugin =
         activePerspective.getClass().getAnnotation(HopPerspectivePlugin.class);
     if (plugin != null) {
-      try {
-        
EnvironmentUtils.getInstance().openUrl(getDocUrl(plugin.documentationUrl()));
-      } catch (Exception e) {
-        new ErrorDialog(shell, "Error", "Error opening URL", e);
-      }
+      HelpUtils.openHelp(shell, getDocUrl(plugin.documentationUrl()));
     }
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfig.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfig.java
index a14fd63aa9..b55c11d6f2 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfig.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfig.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -17,6 +17,14 @@
 
 package org.apache.hop.ui.hopgui.perspective.explorer.config;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.ui.util.HelpOpenMode;
+
+@Getter
+@Setter
 public class ExplorerPerspectiveConfig {
 
   public static final String HOP_CONFIG_EXPLORER_PERSPECTIVE_CONFIG_KEY = 
"explorer-perspective";
@@ -24,14 +32,14 @@ public class ExplorerPerspectiveConfig {
   private String lazyLoadingDepth;
   private String fileLoadingMaxSize;
   private Boolean fileExplorerVisibleByDefault;
-  private Boolean openingHelpFiles;
+  private HelpOpenMode helpOpenMode;
   private Boolean activeFileSelection;
 
   public ExplorerPerspectiveConfig() {
     this.lazyLoadingDepth = "0";
     this.fileLoadingMaxSize = "16";
     this.fileExplorerVisibleByDefault = true;
-    this.openingHelpFiles = false;
+    this.helpOpenMode = HelpOpenMode.BROWSER;
     this.activeFileSelection = true;
   }
 
@@ -40,47 +48,33 @@ public class ExplorerPerspectiveConfig {
     this.lazyLoadingDepth = config.lazyLoadingDepth;
     this.fileLoadingMaxSize = config.fileLoadingMaxSize;
     this.fileExplorerVisibleByDefault = config.fileExplorerVisibleByDefault;
-    this.openingHelpFiles = config.openingHelpFiles;
+    this.helpOpenMode = config.getHelpOpenMode();
     this.activeFileSelection = config.activeFileSelection;
   }
 
-  public String getLazyLoadingDepth() {
-    return lazyLoadingDepth;
-  }
-
-  public void setLazyLoadingDepth(String lazyLoadingDepth) {
-    this.lazyLoadingDepth = lazyLoadingDepth;
-  }
-
-  public String getFileLoadingMaxSize() {
-    return fileLoadingMaxSize;
-  }
-
-  public void setFileLoadingMaxSize(String fileLoadingMaxSize) {
-    this.fileLoadingMaxSize = fileLoadingMaxSize;
+  public HelpOpenMode getHelpOpenMode() {
+    return helpOpenMode != null ? helpOpenMode : HelpOpenMode.BROWSER;
   }
 
-  public Boolean getFileExplorerVisibleByDefault() {
-    return fileExplorerVisibleByDefault;
-  }
-
-  public void setFileExplorerVisibleByDefault(Boolean 
fileExplorerVisibleByDefault) {
-    this.fileExplorerVisibleByDefault = fileExplorerVisibleByDefault;
+  /**
+   * Legacy hop-config key {@code openingHelpFiles}. True used to mean "open 
help in Explorer tabs".
+   *
+   * @param openingHelpFiles previous boolean flag
+   */
+  @JsonSetter("openingHelpFiles")
+  public void migrateOpeningHelpFiles(Boolean openingHelpFiles) {
+    if (Boolean.TRUE.equals(openingHelpFiles) && this.helpOpenMode == 
HelpOpenMode.BROWSER) {
+      this.helpOpenMode = HelpOpenMode.TAB;
+    }
   }
 
+  /**
+   * @return true when help should open as an Explorer tab (legacy checkbox 
semantics)
+   * @deprecated use {@link #getHelpOpenMode()}
+   */
+  @Deprecated(since = "2.20")
+  @JsonIgnore
   public Boolean isOpeningHelpFiles() {
-    return openingHelpFiles != null ? openingHelpFiles : false;
-  }
-
-  public void setOpeningHelpFiles(Boolean openingHelpFiles) {
-    this.openingHelpFiles = openingHelpFiles;
-  }
-
-  public Boolean getActiveFileSelection() {
-    return activeFileSelection != null ? activeFileSelection : true;
-  }
-
-  public void setActiveFileSelection(Boolean activeFileSelection) {
-    this.activeFileSelection = activeFileSelection;
+    return getHelpOpenMode() == HelpOpenMode.TAB;
   }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
index 9c6218dcf4..699b3382f9 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
@@ -17,6 +17,8 @@
 
 package org.apache.hop.ui.hopgui.perspective.explorer.config;
 
+import java.util.ArrayList;
+import java.util.List;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.config.plugin.ConfigPlugin;
 import org.apache.hop.core.config.plugin.IConfigOptions;
@@ -27,13 +29,17 @@ import org.apache.hop.core.gui.plugin.GuiWidgetElement;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.metadata.api.IHasHopMetadataProvider;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
 import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.apache.hop.ui.core.widget.ComboVar;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.hopgui.HopGui;
 import 
org.apache.hop.ui.hopgui.perspective.configuration.tabs.ConfigPluginOptionsTab;
+import org.apache.hop.ui.util.HelpOpenMode;
 import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
 import org.eclipse.swt.widgets.Control;
 import picocli.CommandLine;
 
@@ -93,12 +99,20 @@ public class ExplorerPerspectiveConfigPlugin
   @GuiWidgetElement(
       id = WIDGET_ID_OPEN_HELP_FILES,
       parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
-      type = GuiElementType.CHECKBOX,
-      label = "i18n::ExplorerPerspectiveConfig.OpenHelpFiles.Label",
-      toolTip = "i18n::ExplorerPerspectiveConfig.OpenHelpFiles.Tooltip")
+      type = GuiElementType.COMBO,
+      variables = false,
+      comboValuesMethod = "getHelpOpenModeLabels",
+      label = "i18n::ExplorerPerspectiveConfig.HelpOpenMode.Label",
+      toolTip = "i18n::ExplorerPerspectiveConfig.HelpOpenMode.Tooltip")
+  @CommandLine.Option(
+      names = {"--open-help-mode"},
+      description = "Where to open help pages: BROWSER, TAB or DIALOG")
+  private String helpOpenMode;
+
   @CommandLine.Option(
       names = {"-oh", "--open-help-in-tabs"},
-      description = "Open help files in Hop GUI tabs instead of external 
browser")
+      description =
+          "Deprecated: open help files in Hop GUI tabs instead of the external 
browser. Prefer --open-help-mode=TAB")
   private Boolean openingHelpFiles;
 
   @GuiWidgetElement(
@@ -137,7 +151,7 @@ public class ExplorerPerspectiveConfigPlugin
     instance.fileLoadingMaxSize = config.getFileLoadingMaxSize();
     Boolean visibleByDefault = config.getFileExplorerVisibleByDefault();
     instance.fileExplorerVisibleByDefault = visibleByDefault != null ? 
visibleByDefault : true;
-    instance.openingHelpFiles = config.isOpeningHelpFiles();
+    instance.helpOpenMode = config.getHelpOpenMode().getLabel();
     instance.activeFileSelection = config.getActiveFileSelection();
     instance.maxUndo = 
Integer.toString(org.apache.hop.ui.core.PropsUi.getInstance().getMaxUndo());
 
@@ -177,10 +191,16 @@ public class ExplorerPerspectiveConfigPlugin
         changed = true;
       }
 
-      if (openingHelpFiles != null) {
-        config.setOpeningHelpFiles(openingHelpFiles);
-        log.logBasic(
-            "Explorer perspective: open help files in tabs is set to '" + 
openingHelpFiles + "'");
+      if (Boolean.TRUE.equals(openingHelpFiles)) {
+        config.setHelpOpenMode(HelpOpenMode.TAB);
+        log.logBasic("Explorer perspective: open help mode is set to '" + 
HelpOpenMode.TAB + "'");
+        changed = true;
+      }
+
+      if (helpOpenMode != null) {
+        HelpOpenMode mode = HelpOpenMode.fromConfigValue(helpOpenMode);
+        config.setHelpOpenMode(mode);
+        log.logBasic("Explorer perspective: open help mode is set to '" + mode 
+ "'");
         changed = true;
       }
 
@@ -243,8 +263,9 @@ public class ExplorerPerspectiveConfigPlugin
               .setFileExplorerVisibleByDefault(fileExplorerVisibleByDefault);
           break;
         case WIDGET_ID_OPEN_HELP_FILES:
-          openingHelpFiles = ((Button) control).getSelection();
-          
ExplorerPerspectiveConfigSingleton.getConfig().setOpeningHelpFiles(openingHelpFiles);
+          helpOpenMode = readComboText(control);
+          ExplorerPerspectiveConfigSingleton.getConfig()
+              .setHelpOpenMode(HelpOpenMode.fromLabel(helpOpenMode));
           break;
         case WIDGET_ID_ACTIVE_FILE_SELECTION:
           activeFileSelection = ((Button) control).getSelection();
@@ -296,6 +317,14 @@ public class ExplorerPerspectiveConfigPlugin
     this.fileExplorerVisibleByDefault = fileExplorerVisibleByDefault;
   }
 
+  public String getHelpOpenMode() {
+    return helpOpenMode;
+  }
+
+  public void setHelpOpenMode(String helpOpenMode) {
+    this.helpOpenMode = helpOpenMode;
+  }
+
   public Boolean isOpeningHelpFiles() {
     return openingHelpFiles != null ? openingHelpFiles : false;
   }
@@ -304,6 +333,32 @@ public class ExplorerPerspectiveConfigPlugin
     this.openingHelpFiles = openingHelpFiles;
   }
 
+  /**
+   * Combo values for {@link HelpOpenMode} shown in the Configuration 
perspective.
+   *
+   * @param log unused (required by GuiCompositeWidgets)
+   * @param metadataProvider unused (required by GuiCompositeWidgets)
+   * @return translated labels in enum order
+   */
+  public List<String> getHelpOpenModeLabels(
+      ILogChannel log, IHopMetadataProvider metadataProvider) {
+    List<String> labels = new ArrayList<>();
+    for (HelpOpenMode mode : HelpOpenMode.values()) {
+      labels.add(mode.getLabel());
+    }
+    return labels;
+  }
+
+  private static String readComboText(Control control) {
+    if (control instanceof Combo combo) {
+      return combo.getText();
+    }
+    if (control instanceof ComboVar comboVar) {
+      return comboVar.getText();
+    }
+    return "";
+  }
+
   public Boolean getActiveFileSelection() {
     return activeFileSelection != null ? activeFileSelection : true;
   }
diff --git a/ui/src/main/java/org/apache/hop/ui/util/HelpOpenMode.java 
b/ui/src/main/java/org/apache/hop/ui/util/HelpOpenMode.java
new file mode 100644
index 0000000000..3915dd9595
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/util/HelpOpenMode.java
@@ -0,0 +1,85 @@
+/*
+ * 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.util;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.i18n.BaseMessages;
+
+/** Where Hop GUI should open documentation / help URLs. */
+public enum HelpOpenMode {
+  /** System (or RAP) browser. */
+  BROWSER,
+  /** HTML tab in the File Explorer perspective. */
+  TAB,
+  /** Modeless dialog parented to the current context shell. */
+  DIALOG;
+
+  private static final Class<?> PKG = HelpOpenMode.class;
+
+  public String getLabel() {
+    return BaseMessages.getString(PKG, "HelpOpenMode." + name());
+  }
+
+  @JsonValue
+  public String toConfigValue() {
+    return name();
+  }
+
+  /**
+   * Resolve a combo label or enum name. Unknown values fall back to {@link 
#BROWSER}.
+   *
+   * @param label translated combo text, enum name, or {@code null}
+   * @return matching mode, never {@code null}
+   */
+  public static HelpOpenMode fromLabel(String label) {
+    if (Utils.isEmpty(label)) {
+      return BROWSER;
+    }
+    String trimmed = label.trim();
+    for (HelpOpenMode mode : values()) {
+      if (mode.name().equalsIgnoreCase(trimmed) || 
mode.getLabel().equalsIgnoreCase(trimmed)) {
+        return mode;
+      }
+    }
+    return BROWSER;
+  }
+
+  /**
+   * Parse a hop-config / CLI value. Accepts enum names and the legacy boolean 
{@code true} (tab
+   * mode). Unknown values fall back to {@link #BROWSER}.
+   *
+   * @param value stored string or {@code null}
+   * @return matching mode, never {@code null}
+   */
+  @JsonCreator
+  public static HelpOpenMode fromConfigValue(String value) {
+    if (Utils.isEmpty(value)) {
+      return BROWSER;
+    }
+    String trimmed = value.trim();
+    if ("true".equalsIgnoreCase(trimmed)) {
+      return TAB;
+    }
+    if ("false".equalsIgnoreCase(trimmed)) {
+      return BROWSER;
+    }
+    return fromLabel(trimmed);
+  }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/util/HelpUtils.java 
b/ui/src/main/java/org/apache/hop/ui/util/HelpUtils.java
index 72cacfe5d0..8c5e1f0ee2 100644
--- a/ui/src/main/java/org/apache/hop/ui/util/HelpUtils.java
+++ b/ui/src/main/java/org/apache/hop/ui/util/HelpUtils.java
@@ -34,6 +34,7 @@ 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.dialog.MessageBox;
+import org.apache.hop.ui.core.dialog.ShowHelpDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.HopFileTypeRegistry;
@@ -57,15 +58,7 @@ public class HelpUtils {
 
   public static Button createHelpButton(final Composite parent, final String 
url) {
     Button button = newButton(parent);
-    button.addListener(
-        SWT.Selection,
-        e -> {
-          try {
-            EnvironmentUtils.getInstance().openUrl(url);
-          } catch (Exception ex) {
-            new ErrorDialog(parent.getShell(), "Error", "Error opening URL", 
ex);
-          }
-        });
+    button.addListener(SWT.Selection, e -> openHelp(parent.getShell(), url));
     return button;
   }
 
@@ -96,17 +89,7 @@ public class HelpUtils {
       return;
     }
     if (isPluginDocumented(plugin)) {
-      try {
-        String originalUrl = getDocUrl(plugin.getDocumentationUrl());
-        String trackedUrl = appendUtmParameters(originalUrl);
-        if 
(ExplorerPerspectiveConfigSingleton.getConfig().isOpeningHelpFiles()) {
-          openHelpInTab(trackedUrl);
-        } else {
-          EnvironmentUtils.getInstance().openUrl(trackedUrl);
-        }
-      } catch (Exception ex) {
-        new ErrorDialog(shell, "Error", "Error opening URL", ex);
-      }
+      openHelp(shell, getDocUrl(plugin.getDocumentationUrl()));
     } else {
       MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
       String msg = "";
@@ -127,12 +110,54 @@ public class HelpUtils {
     }
   }
 
+  /**
+   * Open a documentation URL using the configured {@link HelpOpenMode}.
+   *
+   * @param shell context shell (transform/action dialog or main window)
+   * @param url documentation URL
+   */
+  public static void openHelp(Shell shell, String url) {
+    if (Utils.isEmpty(url)) {
+      return;
+    }
+    try {
+      openTrackedUrl(shell, appendUtmParameters(url));
+    } catch (Exception ex) {
+      Shell errorShell = shell != null ? shell : 
HopGui.getInstance().getShell();
+      new ErrorDialog(errorShell, "Error", "Error opening URL", ex);
+    }
+  }
+
+  static void openTrackedUrl(Shell shell, String trackedUrl) throws 
HopException {
+    HelpOpenMode mode = currentOpenMode();
+    switch (mode) {
+      case TAB:
+        openHelpInTab(trackedUrl);
+        break;
+      case DIALOG:
+        openHelpInDialog(shell, trackedUrl);
+        break;
+      case BROWSER:
+      default:
+        EnvironmentUtils.getInstance().openUrl(trackedUrl);
+        break;
+    }
+  }
+
+  static HelpOpenMode currentOpenMode() {
+    try {
+      return ExplorerPerspectiveConfigSingleton.getConfig().getHelpOpenMode();
+    } catch (Exception e) {
+      return HelpOpenMode.BROWSER;
+    }
+  }
+
   /**
    * Add analytics tracking parameters for help-button <code>
    * mtm_campaign=hopgui&mtm_source=help_btn&mtm_kwd=write to log
    * </code>
    */
-  private static String appendUtmParameters(String url) {
+  static String appendUtmParameters(String url) {
     if (url == null || url.isEmpty()) {
       return url;
     }
@@ -168,4 +193,41 @@ public class HelpUtils {
     // Fallback
     EnvironmentUtils.getInstance().openUrl(url);
   }
+
+  private static void openHelpInDialog(Shell shell, String url) throws 
HopException {
+    Shell parent = resolveParentShell(shell);
+    if (parent == null) {
+      EnvironmentUtils.getInstance().openUrl(url);
+      return;
+    }
+
+    Object existing = parent.getData(ShowHelpDialog.SHELL_DATA_KEY);
+    if (existing instanceof ShowHelpDialog dialog && !dialog.isDisposed()) {
+      dialog.setUrl(url);
+      dialog.forceActive();
+      return;
+    }
+
+    try {
+      ShowHelpDialog dialog = new ShowHelpDialog(parent, url);
+      dialog.open();
+      parent.setData(ShowHelpDialog.SHELL_DATA_KEY, dialog);
+      parent.addDisposeListener(e -> 
parent.setData(ShowHelpDialog.SHELL_DATA_KEY, null));
+    } catch (Exception ex) {
+      parent.setData(ShowHelpDialog.SHELL_DATA_KEY, null);
+      new ErrorDialog(parent, "Error", "Error opening help dialog", ex);
+      EnvironmentUtils.getInstance().openUrl(url);
+    }
+  }
+
+  private static Shell resolveParentShell(Shell shell) {
+    if (shell != null && !shell.isDisposed()) {
+      return shell;
+    }
+    HopGui hopGui = HopGui.getInstance();
+    if (hopGui != null && hopGui.getShell() != null && 
!hopGui.getShell().isDisposed()) {
+      return hopGui.getShell();
+    }
+    return null;
+  }
 }
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 a9fa08b436..30c6e7ab85 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
@@ -55,6 +55,7 @@ HopGui.Documentation.Hop.Title=Hop Documentation
 HopGui.Documentation.Tooltip.Back=Show previous page
 HopGui.Documentation.Tooltip.Forward=Show next page
 HopGui.Documentation.Tooltip.Home=Go to Hop Help home page
+HopGui.Documentation.Tooltip.OpenExternal=Open this page in the system browser
 HopGui.Documentation.Tooltip.Print=Print page
 HopGui.Documentation.Tooltip.Refresh=Refresh page
 HopGui.ErrorDialog.ErrorEditingAction.Message=Error editing action
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_en_US.properties
index 8147d0c3a2..4f2f67e69a 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_en_US.properties
@@ -22,8 +22,8 @@ ExplorerPerspectiveConfig.LazyLoading.Tooltip=The initial 
depth to load not lazi
 ExplorerPerspectiveConfig.FileExplorerVisible.Label=Show file explorer panel 
by default
 ExplorerPerspectiveConfig.FileExplorerVisible.Tooltip=When enabled, the file 
explorer panel (project tree) is shown by default when opening the explorer 
perspective
 
-ExplorerPerspectiveConfig.OpenHelpFiles.Label=Open help files in Hop GUI tabs 
instead of external browser
-ExplorerPerspectiveConfig.OpenHelpFiles.Tooltip=When checked, help links will 
open as a new tab within the Hop GUI using the internal HTML viewer. Unchecked 
will open the system default browser.
+ExplorerPerspectiveConfig.HelpOpenMode.Label=Open help pages in
+ExplorerPerspectiveConfig.HelpOpenMode.Tooltip=Choose where documentation 
opens: the system browser, a tab in the File Explorer perspective, or a dialog 
on top of the current window.
 
 ExplorerPerspectiveConfig.ActiveFileSelection.Label=Select active file in tree 
automatically
 ExplorerPerspectiveConfig.ActiveFileSelection.Tooltip=Automatically select the 
active tab file in the file explorer tree on the left hand side when it is 
shown in a tab.
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_fr_FR.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_fr_FR.properties
index c0feb83e1d..6de04f0bb0 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_fr_FR.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_fr_FR.properties
@@ -20,9 +20,9 @@
 ExplorerPerspectiveConfig.FileSize.Label=Taille maximale des fichiers \u00E0 
charger
 ExplorerPerspectiveConfig.FileExplorerVisible.Label=Afficher le panneau de 
l''explorateur de fichiers par d\u00E9faut
 ExplorerPerspectiveConfig.FileExplorerVisible.Tooltip=Lorsque cette option est 
activ\u00E9e, le panneau de l'explorateur de fichiers (arborescence du projet) 
s'affiche par d\u00E9faut \u00E0 l'ouverture de la perspective
-ExplorerPerspectiveConfig.OpenHelpFiles.Label=Ouvrir l''aide dans Hop 
plut\u00F4t que dans un navigateur externe
+ExplorerPerspectiveConfig.HelpOpenMode.Label=Ouvrir l''aide dans
 ExplorerPerspectiveConfig.FileSize.Tooltip=Taille maximale des fichiers \u00E0 
charger
-ExplorerPerspectiveConfig.OpenHelpFiles.Tooltip=Lorsque cette option est 
coch\u00E9e, l''aide s''ouvrent dans un nouvel onglet de Hop \u00E0 l''aide de 
la visionneuse HTML interne., sinon le navigateur par d\u00E9faut du 
syst\u00E8me est utilis\u00E9.
+ExplorerPerspectiveConfig.HelpOpenMode.Tooltip=Choisissez o\u00F9 s''ouvre la 
documentation : le navigateur du syst\u00E8me, un onglet de l''explorateur de 
fichiers, ou une fen\u00EAtre au-dessus du contexte actuel.
 
 ExplorerPerspectiveConfig.ActiveFileSelection.Label=S\u00E9lectionner 
automatiquement le fichier actif dans l''arborescence
 ExplorerPerspectiveConfig.ActiveFileSelection.Tooltip=S\u00E9lectionner 
automatiquement le fichier de l''onglet actif dans l''explorateur de fichiers 
de gauche lorsqu''il est affich\u00E9.
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_pt_BR.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_pt_BR.properties
index 2fe4eef0ca..c6a437d037 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_pt_BR.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_pt_BR.properties
@@ -23,8 +23,8 @@ ExplorerPerspectiveConfig.LazyLoading.Label=A profundidade 
inicial para carregar
 ExplorerPerspectiveConfig.LazyLoading.Tooltip=A profundidade inicial para 
carregar
 ExplorerPerspectiveConfig.FileExplorerVisible.Label=Mostrar painel explorador 
de arquivos por padrão
 ExplorerPerspectiveConfig.FileExplorerVisible.Tooltip=Quando habilitado, o 
painel explorador de arquivos (árvore do projeto) é mostrado por padrão ao 
abrir a perspectiva exploradora
-ExplorerPerspectiveConfig.OpenHelpFiles.Label=Abra arquivos de ajuda nas abas 
do Hop GUI em vez de navegador externo
-ExplorerPerspectiveConfig.OpenHelpFiles.Tooltip=Quando marcada, links de ajuda 
abrirão como uma nova guia dentro da GUI Hop usando o visualizador HTML 
interno. Sem controle, abrirá o navegador padrão do sistema.
+ExplorerPerspectiveConfig.HelpOpenMode.Label=Abrir p\u00E1ginas de ajuda em
+ExplorerPerspectiveConfig.HelpOpenMode.Tooltip=Escolha onde a 
documenta\u00E7\u00E3o abre: o navegador do sistema, uma aba na perspectiva 
Explorador de arquivos, ou uma janela sobre o contexto atual.
 
 ExplorerPerspectiveConfig.ActiveFileSelection.Label=Selecionar automaticamente 
arquivo ativo na \u00E1rvore
 ExplorerPerspectiveConfig.ActiveFileSelection.Tooltip=Selecionar 
automaticamente o arquivo da guia ativa na \u00E1rvore do explorador de 
arquivos no lado esquerdo.
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_en_US.properties
new file mode 100644
index 0000000000..d0c4cbf365
--- /dev/null
+++ 
b/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_en_US.properties
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+HelpOpenMode.BROWSER=External browser
+HelpOpenMode.TAB=Hop GUI tab
+HelpOpenMode.DIALOG=Dialog window
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_fr_FR.properties
 
b/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_fr_FR.properties
new file mode 100644
index 0000000000..bac4e1bf2d
--- /dev/null
+++ 
b/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_fr_FR.properties
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+HelpOpenMode.BROWSER=Navigateur externe
+HelpOpenMode.TAB=Onglet Hop GUI
+HelpOpenMode.DIALOG=Fen\u00EAtre de dialogue
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_pt_BR.properties
 
b/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_pt_BR.properties
new file mode 100644
index 0000000000..4dd24e1ce7
--- /dev/null
+++ 
b/ui/src/main/resources/org/apache/hop/ui/util/messages/messages_pt_BR.properties
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+HelpOpenMode.BROWSER=Navegador externo
+HelpOpenMode.TAB=Aba do Hop GUI
+HelpOpenMode.DIALOG=Janela de di\u00E1logo
diff --git 
a/ui/src/test/java/org/apache/hop/ui/core/dialog/ShowHelpDialogTest.java 
b/ui/src/test/java/org/apache/hop/ui/core/dialog/ShowHelpDialogTest.java
new file mode 100644
index 0000000000..5b526ce409
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/core/dialog/ShowHelpDialogTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.dialog;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.eclipse.swt.SWT;
+import org.junit.jupiter.api.Test;
+
+class ShowHelpDialogTest {
+
+  @Test
+  void helpDialogStyleIsModelessAndResizable() {
+    int style = ShowHelpDialog.helpDialogStyle();
+    assertEquals(0, style & SWT.APPLICATION_MODAL);
+    assertEquals(0, style & SWT.PRIMARY_MODAL);
+    assertNotEquals(0, style & SWT.RESIZE);
+  }
+}
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigTest.java
new file mode 100644
index 0000000000..b091ffdbfe
--- /dev/null
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.hopgui.perspective.explorer.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.hop.core.json.HopJson;
+import org.apache.hop.ui.util.HelpOpenMode;
+import org.junit.jupiter.api.Test;
+
+class ExplorerPerspectiveConfigTest {
+
+  private final ObjectMapper mapper = HopJson.newMapper();
+
+  @Test
+  void defaultModeIsBrowser() {
+    assertEquals(HelpOpenMode.BROWSER, new 
ExplorerPerspectiveConfig().getHelpOpenMode());
+  }
+
+  @Test
+  void migratesLegacyOpeningHelpFilesTrueToTab() throws Exception {
+    ExplorerPerspectiveConfig config =
+        mapper.readValue(
+            "{\"lazyLoadingDepth\":\"0\",\"openingHelpFiles\":true}",
+            ExplorerPerspectiveConfig.class);
+    assertEquals(HelpOpenMode.TAB, config.getHelpOpenMode());
+  }
+
+  @Test
+  void migratesLegacyOpeningHelpFilesFalseToBrowser() throws Exception {
+    ExplorerPerspectiveConfig config =
+        mapper.readValue("{\"openingHelpFiles\":false}", 
ExplorerPerspectiveConfig.class);
+    assertEquals(HelpOpenMode.BROWSER, config.getHelpOpenMode());
+  }
+
+  @Test
+  void missingLegacyFlagDefaultsToBrowser() throws Exception {
+    ExplorerPerspectiveConfig config =
+        mapper.readValue("{\"lazyLoadingDepth\":\"2\"}", 
ExplorerPerspectiveConfig.class);
+    assertEquals(HelpOpenMode.BROWSER, config.getHelpOpenMode());
+  }
+
+  @Test
+  void newHelpOpenModeWinsOverLegacyBoolean() throws Exception {
+    ExplorerPerspectiveConfig config =
+        mapper.readValue(
+            "{\"helpOpenMode\":\"DIALOG\",\"openingHelpFiles\":true}",
+            ExplorerPerspectiveConfig.class);
+    assertEquals(HelpOpenMode.DIALOG, config.getHelpOpenMode());
+  }
+
+  @Test
+  void serializesHelpOpenModeAndOmitsLegacyBoolean() throws Exception {
+    ExplorerPerspectiveConfig config = new ExplorerPerspectiveConfig();
+    config.setHelpOpenMode(HelpOpenMode.DIALOG);
+    String json = mapper.writeValueAsString(config);
+    assertEquals(
+        HelpOpenMode.DIALOG,
+        mapper.readValue(json, 
ExplorerPerspectiveConfig.class).getHelpOpenMode());
+    assertFalse(json.contains("openingHelpFiles"));
+    assertFalse(json.contains("\"helpOpenMode\":\"true\""));
+  }
+
+  @Test
+  void copyConstructorCopiesHelpOpenMode() {
+    ExplorerPerspectiveConfig original = new ExplorerPerspectiveConfig();
+    original.setHelpOpenMode(HelpOpenMode.TAB);
+    assertEquals(HelpOpenMode.TAB, new 
ExplorerPerspectiveConfig(original).getHelpOpenMode());
+  }
+}
diff --git a/ui/src/test/java/org/apache/hop/ui/util/HelpOpenModeTest.java 
b/ui/src/test/java/org/apache/hop/ui/util/HelpOpenModeTest.java
new file mode 100644
index 0000000000..74c9aba0b6
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/util/HelpOpenModeTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class HelpOpenModeTest {
+
+  @Test
+  void fromConfigValueAcceptsEnumNamesAndLegacyBoolean() {
+    assertEquals(HelpOpenMode.BROWSER, HelpOpenMode.fromConfigValue(null));
+    assertEquals(HelpOpenMode.BROWSER, HelpOpenMode.fromConfigValue(""));
+    assertEquals(HelpOpenMode.BROWSER, 
HelpOpenMode.fromConfigValue("BROWSER"));
+    assertEquals(HelpOpenMode.TAB, HelpOpenMode.fromConfigValue("tab"));
+    assertEquals(HelpOpenMode.DIALOG, HelpOpenMode.fromConfigValue("Dialog"));
+    assertEquals(HelpOpenMode.TAB, HelpOpenMode.fromConfigValue("true"));
+    assertEquals(HelpOpenMode.BROWSER, HelpOpenMode.fromConfigValue("false"));
+    assertEquals(HelpOpenMode.BROWSER, 
HelpOpenMode.fromConfigValue("not-a-mode"));
+  }
+
+  @Test
+  void fromLabelAcceptsEnumNameAndTranslatedLabel() {
+    assertEquals(HelpOpenMode.BROWSER, HelpOpenMode.fromLabel(null));
+    assertEquals(HelpOpenMode.DIALOG, HelpOpenMode.fromLabel("DIALOG"));
+    assertEquals(HelpOpenMode.TAB, 
HelpOpenMode.fromLabel(HelpOpenMode.TAB.getLabel()));
+    assertEquals(HelpOpenMode.DIALOG, 
HelpOpenMode.fromLabel(HelpOpenMode.DIALOG.getLabel()));
+  }
+
+  @Test
+  void toConfigValueIsEnumName() {
+    assertEquals("BROWSER", HelpOpenMode.BROWSER.toConfigValue());
+    assertEquals("TAB", HelpOpenMode.TAB.toConfigValue());
+    assertEquals("DIALOG", HelpOpenMode.DIALOG.toConfigValue());
+  }
+}
diff --git a/ui/src/test/java/org/apache/hop/ui/util/HelpUtilsTest.java 
b/ui/src/test/java/org/apache/hop/ui/util/HelpUtilsTest.java
new file mode 100644
index 0000000000..c175c67dd4
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/util/HelpUtilsTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class HelpUtilsTest {
+
+  @Test
+  void appendUtmParametersAddsQueryWhenMissing() {
+    String tracked = 
HelpUtils.appendUtmParameters("https://hop.apache.org/manual/latest/x.html";);
+    
assertTrue(tracked.startsWith("https://hop.apache.org/manual/latest/x.html?";));
+    assertTrue(tracked.contains("mtm_campaign="));
+    assertTrue(tracked.contains("mtm_source="));
+    assertFalse(tracked.contains("?mtm_campaign") && 
tracked.contains("&mtm_campaign="));
+  }
+
+  @Test
+  void appendUtmParametersUsesAmpersandWhenQueryExists() {
+    String tracked =
+        
HelpUtils.appendUtmParameters("https://hop.apache.org/manual/latest/x.html?foo=bar";);
+    assertTrue(tracked.contains("?foo=bar&mtm_campaign="));
+  }
+
+  @Test
+  void appendUtmParametersPassesThroughBlank() {
+    assertEquals("", HelpUtils.appendUtmParameters(""));
+    assertEquals(null, HelpUtils.appendUtmParameters(null));
+  }
+}

Reply via email to