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 97898852ea Issue #8486 : Harmonize the bottom panel and add an option
to disable the embedded terminal (#8575)
97898852ea is described below
commit 97898852ea74a1edec00b77522729335fc180bae
Author: Matt Casters <[email protected]>
AuthorDate: Fri Sep 25 08:09:47 2026 +0200
Issue #8486 : Harmonize the bottom panel and add an option to disable the
embedded terminal (#8575)
* Issue #8486 : Harmonize the bottom panel and add an option to disable the
embedded terminal
The bottom-left terminal button is now a panel button, with sidebar buttons
for the terminal, search, the database workbench, and the VFS file explorer. An
open tool stays highlighted while the panel is visible. Enable embedded
terminal on the General tab turns the built-in terminal off without removing
the other tools.
* Issue #8486 : Open bottom-panel tools from the + tab
The terminal, search, database, and VFS buttons toggled the panel instead
of adding a tab. Those buttons are gone. The + tab on the panel opens a menu
for a new terminal, search, database, or VFS file explorer tab.
* Issue #8486 : Show the bottom panel and its tabs in the user manual
---
.../assets/images/hop-gui/bottom-panel-empty.png | Bin 0 -> 19428 bytes
.../assets/images/hop-gui/bottom-panel-tabs.png | Bin 0 -> 90347 bytes
.../ROOT/pages/hop-gui/disable-ui-elements.adoc | 6 +-
.../modules/ROOT/pages/hop-gui/index.adoc | 26 +
.../ROOT/pages/hop-gui/perspective-ai-advisor.adoc | 2 +-
.../pages/hop-gui/perspective-configuration.adoc | 4 +
.../ROOT/pages/hop-gui/perspective-database.adoc | 1 +
.../ROOT/pages/hop-gui/perspective-search.adoc | 1 +
.../ROOT/pages/hop-gui/vfs-file-explorer.adoc | 2 +-
.../main/java/org/apache/hop/ui/core/PropsUi.java | 16 +
.../main/java/org/apache/hop/ui/hopgui/HopGui.java | 71 ++-
.../ui/hopgui/SidebarToolbarItemDescriptor.java | 2 +-
.../tabs/ConfigGeneralOptionsTab.java | 41 ++
.../ui/hopgui/search/ReferenceSearchResults.java | 1 +
.../ui/hopgui/search/SearchEverywhereDialog.java | 1 +
.../hop/ui/hopgui/terminal/HopGuiBottomDock.java | 629 ++++++++++++++++-----
.../hopgui/vfs/explorer/VfsFileExplorerViews.java | 43 +-
.../core/dialog/messages/messages_en_US.properties | 3 +
.../ui/hopgui/messages/messages_en_US.properties | 1 +
.../terminal/messages/messages_en_US.properties | 6 +-
.../ui/hopgui/terminal/HopGuiBottomDockTest.java | 9 +
.../vfs/explorer/VfsFileExplorerViewsTest.java | 33 ++
22 files changed, 727 insertions(+), 171 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/bottom-panel-empty.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/bottom-panel-empty.png
new file mode 100644
index 0000000000..775b8f5718
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/bottom-panel-empty.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/bottom-panel-tabs.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/bottom-panel-tabs.png
new file mode 100644
index 0000000000..807dcf4992
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/bottom-panel-tabs.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/disable-ui-elements.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/disable-ui-elements.adoc
index 077e19f2e1..13e3902220 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/disable-ui-elements.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/disable-ui-elements.adoc
@@ -78,6 +78,10 @@ For each element that needs to be disabled, you can add an
extra `<exclusion>` l
* `@GuiKeyboardShortcut` – use the shortcut's `className#methodName`
* `@HopMetadata` – use the metadata plugin id (from `key` or `name` in the
annotation)
* `@ExtensionPoint` – use the extension point plugin's `id`
-* `@ConfigPlugin` – use the config plugin's `id`
+* `@ConfigPlugin` – use the config plugin's `id`
+
+Excluding `40010-menu-tools-terminal` forces the embedded terminal off for
every user of that installation.
+The bottom panel, search, database workbench, and VFS file explorer stay
available.
+Users who only want to turn their own terminal off can clear *Enable embedded
terminal* on the General tab of the
xref:hop-gui/perspective-configuration.adoc[Configuration perspective].
diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/index.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/index.adoc
index ea41c15584..a37b6ddc89 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/index.adoc
@@ -17,6 +17,7 @@ under the License.
[[HopGui]]
:description: Hop Gui is Hop's visual development environment where you will
mainly design, run and debug workflows and pipelines, but Hop Gui has a lot
more to offer: you can configure all of your metadata items like database
connections, run configurations etc. You can search, manage files, version
control your work and more.
+:imagesdir: ../assets/images
= Hop Gui
@@ -40,3 +41,28 @@ Covered here are:
* xref:hop-gui/shortcuts.adoc[Keyboard Shortcuts]: a list of the keyboard
shortcuts that are available in Hop Gui.
* xref:hop-gui/hop-web.adoc[Hop Web]: a web version of Hop Gui that provides
the exact same functionality and user experience as the desktop version.
For Docker volume mounts and upgrades, see xref:hop-web-docker.adoc[Hop Web in
Docker].
+
+== Bottom panel
+
+The button at the bottom left of the window shows or hides the bottom panel.
+With the panel open and nothing in it, the tab bar shows only *+*.
+
+image::hop-gui/bottom-panel-empty.png[The bottom panel open and empty, with
the + tab.,width="45%"]
+
+Click *+* and choose a tool: Terminal, Search, Database, or VFS File Explorer.
+Terminal and search add another tab each time.
+Database and the xref:hop-gui/vfs-file-explorer.adoc[VFS file explorer] open
their tab, or focus it when it is already there.
+
+image::hop-gui/bottom-panel-tabs.png[Database, VFS File Explorer, and Search
open together in the bottom panel.,width="90%"]
+
+The execution-results button stays above the panel button and is only there in
the file explorer, on a pipeline or workflow.
+
+`CTRL-J` shows the terminal, and hides the panel when a terminal tab is
already selected.
+`CTRL-SHIFT-J` opens another terminal.
+`CTRL-F` still opens Search Everywhere.
+*Show all results* opens a search tab in the bottom panel, and *+* → Search
does the same for an empty query.
+
+In the xref:hop-gui/perspective-configuration.adoc[Configuration perspective],
on the General tab, *Enable embedded terminal* turns the built-in terminal off
on desktops where it misbehaves.
+Search, the database workbench, and the VFS file explorer stay available.
+Hop Web has no embedded terminal.
+An administrator can also force the terminal off by excluding
`40010-menu-tools-terminal` in
xref:hop-gui/disable-ui-elements.adoc[disabledGuiElements.xml].
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-ai-advisor.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-ai-advisor.adoc
index f40180bd83..c14077f3c0 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-ai-advisor.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-ai-advisor.adoc
@@ -226,7 +226,7 @@ The same workbench can run outside the perspective, like
the Database perspectiv
* *AI Help* on a pipeline or workflow toolbar (and canvas *Basic → AI Help*)
opens that floating window so the graph stays visible.
* *Float* on the session toolbar (or **Tools → AI Assistant window**) opens a
non-modal window you can put beside the canvas and the execution log.
-* *Dock* opens it as a tab in the bottom dock.
+* *Dock* opens it as a tab in the bottom dock, in the same panel as the
terminal, search, database, and VFS file explorer.
image::hop-gui/ai-advisor/ai-advisor-floating-window.jpg[AI Assistant floating
over a failed workflow and its execution log,width="90%"]
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 13b3ee8da5..85d04e14a2 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
@@ -60,6 +60,10 @@ INFO: this setting complements the preview option that is
used in the xref:pipel
|Open last file at startup|reopen the last used files and select the last used
file when Hop GUI starts|yes
+|Enable embedded terminal
+a|Show the embedded terminal in the bottom panel. Clear this on desktops where
that terminal misbehaves. Search, the database workbench, and the VFS file
explorer stay available. Hop Web has no embedded terminal. Excluding
`40010-menu-tools-terminal` in
xref:hop-gui/disable-ui-elements.adoc[disabledGuiElements.xml] forces it off.
+|yes
+
|Autosave changed files|Automatically save changed workflow and pipelines
files|yes
|Automatically split hops
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-database.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-database.adoc
index 00ecbcc45f..e5d1757609 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-database.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-database.adoc
@@ -84,6 +84,7 @@ The same workbench can run outside the perspective, like
Search:
* *Float* on the connection toolbar (or **Tools → Database window**) opens a
non-modal window you can put on another monitor while you stay in Data
orchestration.
* *Dock* opens it as a tab in the bottom dock (next to the terminal and search
results).
+*+* on that panel's tab bar does the same.
image::hop-gui/database/database-perspective-popup-dialog.png[The floating
Database window,width="90%"]
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-search.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-search.adoc
index 9f04ed8fc5..85d780c3a3 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-search.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-search.adoc
@@ -44,6 +44,7 @@ Open search with `CTRL-F` or *Edit → Find*. Type in the
search field to filter
Results are grouped by *Open files* and *Project files*, then by type
(Pipeline, Workflow, metadata type, and so on). Use the arrow keys to move,
*Enter* to open the selection, and *Escape* to close the popup.
*Show all results* hands the same query to a search tab in the bottom dock for
deeper browsing (details panel, open selected item).
+*+* on that panel's tab bar opens another search tab.
=== What is searched
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/vfs-file-explorer.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/vfs-file-explorer.adoc
index e75194b923..6a8b694b08 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/vfs-file-explorer.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/vfs-file-explorer.adoc
@@ -22,7 +22,7 @@ under the License.
The VFS File Explorer browses a folder on any xref:vfs.adoc[Apache VFS]
location: the local disk, a zip file, or a named connection such as S3, FTP or
HDFS.
It is not the xref:hop-gui/perspective-file-explorer.adoc[File Explorer
perspective], which stays on the project, and it is not the
xref:hop-gui/file-dialog.adoc[file dialog], which is still what Hop uses to
open and save a file.
-Open it from **Tools → VFS File Explorer**.
+Open it from **Tools → VFS File Explorer**, or from *+* on the bottom panel
tab bar.
The same browser can sit in the bottom panel while you work on a pipeline or
workflow.
Use the dock and undock buttons to move the current location between the
window and that panel.
Each host keeps its own tabs.
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 fa4aa12ae2..4a5c6b9de0 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
@@ -77,6 +77,10 @@ public class PropsUi extends Props {
private static final String HIDE_MENU_BAR = "HideMenuBar";
private static final String SORT_FIELD_BY_NAME = "SortFieldByName";
private static final String CANVAS_GRID_SIZE = "CanvasGridSize";
+
+ /** Absent means the embedded terminal stays on. */
+ public static final String STRING_EMBEDDED_TERMINAL_ENABLED =
"EmbeddedTerminalEnabled";
+
private static final String AUTO_LAYOUT_DIRECTION = "AutoLayoutDirection";
private static final String AUTO_LAYOUT_LAYER_SPACING =
"AutoLayoutLayerSpacing";
private static final String AUTO_LAYOUT_NODE_SPACING =
"AutoLayoutNodeSpacing";
@@ -550,6 +554,18 @@ public class PropsUi extends Props {
return !NO.equalsIgnoreCase(open);
}
+ public void setEmbeddedTerminalEnabled(boolean enabled) {
+ setProperty(STRING_EMBEDDED_TERMINAL_ENABLED, enabled ? YES : NO);
+ }
+
+ /**
+ * True unless the user has turned the embedded terminal off. Hop Web and
{@code
+ * disabledGuiElements.xml} are applied separately by {@code
HopGuiBottomDock}.
+ */
+ public boolean isEmbeddedTerminalEnabled() {
+ return !NO.equalsIgnoreCase(getProperty(STRING_EMBEDDED_TERMINAL_ENABLED));
+ }
+
public void setReloadingFilesOnChange(boolean reload) {
setProperty(RELOAD_FILES_ON_CHANGE, reload ? YES : NO);
}
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 6722a2470d..d65c6b167e 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
@@ -145,6 +145,7 @@ import
org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
import org.apache.hop.ui.hopgui.perspective.metadata.MetadataPerspective;
import org.apache.hop.ui.hopgui.search.HopGuiSearchLocation;
import org.apache.hop.ui.hopgui.search.SearchEverywhereDialog;
+import org.apache.hop.ui.hopgui.terminal.HopGuiBottomDock;
import org.apache.hop.ui.hopgui.welcome.WelcomeDialog;
import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
import org.apache.hop.ui.util.EnvironmentUtils;
@@ -312,8 +313,8 @@ public class HopGui
public static final String SIDEBAR_TOOLBAR_ITEM_EXECUTION_RESULTS =
"HopGui-SidebarToolbar-ExecutionResults";
- /** Id for the terminal toggle button in the sidebar bottom toolbar. */
- public static final String SIDEBAR_TOOLBAR_ITEM_TERMINAL =
"HopGui-SidebarToolbar-Terminal";
+ /** Id for the bottom-panel show/hide button in the sidebar bottom toolbar.
*/
+ public static final String SIDEBAR_TOOLBAR_ITEM_PANEL =
"HopGui-SidebarToolbar-Panel";
public static final String DEFAULT_HOP_GUI_NAMESPACE = "hop-gui";
@@ -379,6 +380,22 @@ public class HopGui
return terminalPanel;
}
+ /**
+ * Apply the embedded-terminal option to the dock, the Tools menu, and the
sidebar button. Safe to
+ * call before the dock exists.
+ */
+ public void applyEmbeddedTerminalOption() {
+ boolean enabled = HopGuiBottomDock.isTerminalCapabilityEnabled();
+ if (terminalPanel != null && !terminalPanel.isDisposed()) {
+ terminalPanel.setTerminalsEnabled(enabled);
+ }
+ if (mainMenuWidgets != null) {
+
mainMenuWidgets.enableMenuItem(HopGuiBottomDock.ID_MAIN_MENU_TOOLS_TERMINAL,
enabled);
+
mainMenuWidgets.enableMenuItem(HopGuiBottomDock.ID_MAIN_MENU_TOOLS_NEW_TERMINAL,
enabled);
+ }
+ refreshBottomToolbarItems();
+ }
+
private static final PrintStream originalSystemOut = System.out;
private static final PrintStream originalSystemErr = System.err;
@@ -2238,24 +2255,25 @@ public class HopGui
fdBottomToolbar.bottom = new FormAttachment(100, -4);
bottomToolbar.setLayoutData(fdBottomToolbar);
- // Register built-in sidebar toolbar items (visibility depends on active
perspective).
- // File explorer: both terminal and execution. Other perspectives:
terminal only.
- // List order: terminal then execution; refresh draws in reverse so
execution appears above.
+ // Register built-in sidebar toolbar items. The first item added sits at
the bottom because
+ // refresh lays the list out in reverse. Execution results stays File
Explorer only and is
+ // added last so it sits above the panel button. Tools in the bottom panel
are added from the
+ // "+" tab, not from here.
int sidebarIconSize = 24;
sidebarToolbarDescriptors.add(
SidebarToolbarItemDescriptor.builder()
- .id(SIDEBAR_TOOLBAR_ITEM_TERMINAL)
- .imagePath("ui/images/terminal.svg")
+ .id(SIDEBAR_TOOLBAR_ITEM_PANEL)
+ .imagePath("ui/images/dock-panel.svg")
.imageSize(sidebarIconSize)
- .tooltip("Toggle Terminal Panel")
+ .tooltip(BaseMessages.getString(PKG,
"HopGui.Sidebar.BottomPanel.Tooltip"))
.onSelect(
() -> {
if (terminalPanel != null) {
- terminalPanel.toggleTerminal();
+ terminalPanel.toggleDock();
}
})
- .selectedSupplier(() -> terminalPanel != null &&
terminalPanel.isTerminalVisible())
- .available(!EnvironmentUtils.getInstance().isWeb())
+ .selectedSupplier(() -> terminalPanel != null &&
terminalPanel.isDockVisible())
+ .available(true)
.build());
sidebarToolbarDescriptors.add(
SidebarToolbarItemDescriptor.builder()
@@ -2297,22 +2315,15 @@ public class HopGui
* Add a main composite where the various perspectives can parent on to show
stuff... Its area is
* to just below the main toolbar and to the right of the perspectives
toolbar.
*
- * <p>Wraps everything in a {@link
org.apache.hop.ui.hopgui.terminal.HopGuiBottomDock} which hosts
- * the perspectives in its top section and a tabbed dock (terminals and
other tools such as the
- * search results) in its bottom section. The integrated terminal is a gated
capability: it is
- * turned off on the web (no AWT/PTY there) and can be disabled in {@code
- * disabledGuiElements.xml}.
+ * <p>Wraps everything in a {@link HopGuiBottomDock} which hosts the
perspectives in its top
+ * section and a tabbed bottom panel (terminal, search, database, VFS file
explorer, and other
+ * tools) below that. The integrated terminal is off on Hop Web, when
excluded in {@code
+ * disabledGuiElements.xml}, and when the user clears Enable embedded
terminal.
*/
private void addMainPerspectivesComposite() {
- boolean terminalsEnabled =
- !EnvironmentUtils.getInstance().isWeb()
- &&
!org.apache.hop.core.gui.plugin.GuiRegistry.getDisabledGuiElements()
- .contains(
-
org.apache.hop.ui.hopgui.terminal.HopGuiBottomDock.ID_MAIN_MENU_TOOLS_TERMINAL);
-
- terminalPanel =
- new org.apache.hop.ui.hopgui.terminal.HopGuiBottomDock(
- mainHopGuiComposite, this, terminalsEnabled);
+ boolean terminalsEnabled = HopGuiBottomDock.isTerminalCapabilityEnabled();
+
+ terminalPanel = new HopGuiBottomDock(mainHopGuiComposite, this,
terminalsEnabled);
FormData fdTerminalPanel = new FormData();
fdTerminalPanel.top = new FormAttachment(0, 0);
fdTerminalPanel.left = new FormAttachment(perspectivesSidebar, 0);
@@ -2338,6 +2349,7 @@ public class HopGui
mainPerspectivesComposite = terminalPanel.getPerspectiveComposite();
mainPerspectivesComposite.setLayout(new StackLayout());
+ applyEmbeddedTerminalOption();
}
public void setUndoMenu(IUndo undoInterface) {
@@ -2799,7 +2811,7 @@ public class HopGui
SWT.MouseDown,
e -> {
if (d.getOnSelect() != null) d.getOnSelect().run();
- updateVisual.run();
+ refreshSidebarToolbarButtonStates();
});
imgLabel.addListener(
SWT.MouseEnter,
@@ -2817,7 +2829,7 @@ public class HopGui
SWT.MouseDown,
e -> {
if (d.getOnSelect() != null) d.getOnSelect().run();
- updateVisual.run();
+ refreshSidebarToolbarButtonStates();
});
updateVisual.run();
@@ -2874,8 +2886,9 @@ public class HopGui
if (d.getOnSelect() != null) {
d.getOnSelect().run();
}
- canvas.setData("selected",
d.getSelectedSupplier().getAsBoolean());
- canvas.redraw();
+ // Refresh every button. Updating only the clicked one left the
others highlighted
+ // after the panel was hidden and shown again.
+ refreshSidebarToolbarButtonStates();
});
}
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/SidebarToolbarItemDescriptor.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/SidebarToolbarItemDescriptor.java
index d499593de5..422afb5507 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/SidebarToolbarItemDescriptor.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/SidebarToolbarItemDescriptor.java
@@ -82,6 +82,6 @@ public class SidebarToolbarItemDescriptor {
*/
@Builder.Default BooleanSupplier selectedSupplier = () -> false;
- /** Whether this item is available (e.g. terminal only when not in web). */
+ /** Whether this item is available (e.g. a button that is always shown). */
@Builder.Default boolean available = true;
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGeneralOptionsTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGeneralOptionsTab.java
index 6400eb0b01..4c2d6e6ee9 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGeneralOptionsTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGeneralOptionsTab.java
@@ -21,6 +21,7 @@ package
org.apache.hop.ui.hopgui.perspective.configuration.tabs;
import org.apache.hop.core.Const;
import org.apache.hop.core.config.HopConfig;
import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiRegistry;
import org.apache.hop.core.gui.plugin.tab.GuiTab;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.ui.core.PropsUi;
@@ -30,6 +31,8 @@ import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.hopgui.HopGui;
import
org.apache.hop.ui.hopgui.perspective.configuration.ConfigurationPerspective;
import org.apache.hop.ui.hopgui.shared.SashFormMemory;
+import org.apache.hop.ui.hopgui.terminal.HopGuiBottomDock;
+import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
@@ -58,6 +61,7 @@ public class ConfigGeneralOptionsTab {
private Text wDefaultPreview;
private Button wUseCache;
private Button wOpenLast;
+ private Button wEmbeddedTerminal;
private Button wReloadFileOnChange;
private Button wAutoSave;
private Button wAutoSplit;
@@ -97,6 +101,11 @@ public class ConfigGeneralOptionsTab {
wDefaultPreview.setText(Integer.toString(props.getDefaultPreviewSize()));
wUseCache.setSelection(props.useDBCache());
wOpenLast.setSelection(props.openLastFile());
+ if (wEmbeddedTerminal != null
+ && !wEmbeddedTerminal.isDisposed()
+ && wEmbeddedTerminal.isEnabled()) {
+ wEmbeddedTerminal.setSelection(props.isEmbeddedTerminalEnabled());
+ }
wReloadFileOnChange.setSelection(props.isReloadingFilesOnChange());
wAutoSave.setSelection(!props.getAutoSave()); // Inverted logic
wCopyDistribute.setSelection(props.showCopyOrDistributeWarning());
@@ -222,6 +231,29 @@ public class ConfigGeneralOptionsTab {
margin);
lastControl = wOpenLast;
+ // Embedded terminal. Hop Web has no PTY, so the checkbox is omitted
there. An exclusion of the
+ // terminal menu forces it off and must not be overwritten when the other
options are saved.
+ if (!EnvironmentUtils.getInstance().isWeb()) {
+ boolean forcedOff =
+ GuiRegistry.getDisabledGuiElements()
+ .contains(HopGuiBottomDock.ID_MAIN_MENU_TOOLS_TERMINAL);
+ wEmbeddedTerminal =
+ createCheckbox(
+ wGeneralComp,
+ "EnterOptionsDialog.EmbeddedTerminal.Label",
+ forcedOff
+ ? "EnterOptionsDialog.EmbeddedTerminal.ForcedOff.ToolTip"
+ : "EnterOptionsDialog.EmbeddedTerminal.ToolTip",
+ !forcedOff && props.isEmbeddedTerminalEnabled(),
+ lastControl,
+ margin);
+ if (forcedOff) {
+ wEmbeddedTerminal.setEnabled(false);
+ wEmbeddedTerminal.setSelection(false);
+ }
+ lastControl = wEmbeddedTerminal;
+ }
+
// Reload file if changed on filesystem?
wReloadFileOnChange =
createCheckbox(
@@ -703,6 +735,15 @@ public class ConfigGeneralOptionsTab {
Const.toInt(wDefaultPreview.getText(), props.getDefaultPreviewSize()));
props.setUseDBCache(wUseCache.getSelection());
props.setOpenLastFile(wOpenLast.getSelection());
+ if (wEmbeddedTerminal != null
+ && !wEmbeddedTerminal.isDisposed()
+ && wEmbeddedTerminal.isEnabled()) {
+ boolean embeddedTerminal = wEmbeddedTerminal.getSelection();
+ if (embeddedTerminal != props.isEmbeddedTerminalEnabled()) {
+ props.setEmbeddedTerminalEnabled(embeddedTerminal);
+ HopGui.getInstance().applyEmbeddedTerminalOption();
+ }
+ }
props.setReloadingFilesOnChange(wReloadFileOnChange.getSelection());
props.setAutoSave(
!wAutoSave
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/ReferenceSearchResults.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/ReferenceSearchResults.java
index ad422b22d0..4c9f7de607 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/ReferenceSearchResults.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/ReferenceSearchResults.java
@@ -101,6 +101,7 @@ public final class ReferenceSearchResults {
}
Control content =
dock.openToolTab(
+ dock.nextSearchToolId(),
tabTitle,
GuiResource.getInstance().getImage("ui/images/search.svg", 16, 16),
true,
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
index 512088a39a..fe474e5359 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
@@ -742,6 +742,7 @@ public class SearchEverywhereDialog {
}
Control content =
dock.openToolTab(
+ dock.nextSearchToolId(),
searchTabTitle(searchString),
GuiResource.getInstance().getImageSearch(),
true,
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDock.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDock.java
index 6f1fcf1011..da258ab488 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDock.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDock.java
@@ -22,6 +22,7 @@ import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.Const;
import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiRegistry;
import org.apache.hop.core.gui.plugin.key.GuiKeyboardShortcut;
import org.apache.hop.core.gui.plugin.key.GuiOsxKeyboardShortcut;
import org.apache.hop.core.gui.plugin.menu.GuiMenuElement;
@@ -38,6 +39,10 @@ import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.HopGuiKeyHandler;
import org.apache.hop.ui.hopgui.perspective.TabClosable;
import org.apache.hop.ui.hopgui.perspective.TabCloseHandler;
+import org.apache.hop.ui.hopgui.perspective.database.DatabaseWorkbenchViews;
+import org.apache.hop.ui.hopgui.search.HopGuiSearchResultsPanel;
+import org.apache.hop.ui.hopgui.search.SearchEverywhereDialog;
+import org.apache.hop.ui.hopgui.vfs.explorer.VfsFileExplorerViews;
import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
@@ -53,19 +58,21 @@ import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
/**
- * Bottom dock for Hop GUI. It wraps the main perspectives composite in a
vertical SashForm, with
+ * Bottom panel for Hop GUI. It wraps the main perspectives composite in a
vertical SashForm, with
* the perspectives in the top section and a tabbed dock in the bottom section
that persists across
* perspective switches.
*
- * <p>The dock hosts two kinds of tabs in one {@link CTabFolder}: terminal
tabs (the integrated
- * command line, a gated capability - see {@link #terminalsEnabled}) and
generic "tool" tabs opened
- * through {@link #focusOrOpenToolTab} (e.g. the search results view).
Terminal-specific behaviour
- * (the "+" tab, font sizing, save/restore) only applies to terminal tabs.
+ * <p>The dock hosts terminal tabs (a capability that can be turned off — see
{@link
+ * #terminalsEnabled}) and generic tool tabs opened through {@link
#focusOrOpenToolTab} (search,
+ * database, VFS file explorer, AI workbench). The "+" tab opens a menu to add
one of those tabs.
+ * Font sizing and terminal save/restore apply only to terminal tabs.
*/
@GuiPlugin(name = "Terminal panel", description = "Terminal panel")
public class HopGuiBottomDock extends Composite implements TabClosable {
@@ -75,6 +82,12 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
public static final String ID_MAIN_MENU_TOOLS_TERMINAL =
"40010-menu-tools-terminal";
public static final String ID_MAIN_MENU_TOOLS_NEW_TERMINAL =
"40020-menu-tools-new-terminal";
+ /** Selected-tool id for a terminal tab. Tool tabs use their own {@code
DATA_TOOL_ID}. */
+ public static final String TOOL_ID_TERMINAL = "terminal";
+
+ /** Tool-id prefix for search-results tabs opened in this dock. */
+ public static final String SEARCH_TOOL_ID_PREFIX = "search-";
+
private final HopGui hopGui;
private SashForm verticalSash;
@@ -83,16 +96,30 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
private Composite terminalComposite;
@Getter private CTabFolder terminalTabs;
private CTabItem newTerminalTab;
+
+ /** Last real tab, so dismissing the "+" menu can leave that tab selected. */
+ private CTabItem lastContentTab;
+
@Getter private boolean terminalVisible = false;
@Getter private int terminalHeightPercent = 35;
private boolean isClearing = false;
private int terminalCounter = 1;
/**
- * Whether the terminal capability is available (turned off on web or via
disabledGuiElements).
+ * Whether the terminal capability is available. Off on Hop Web, when {@code
+ * disabledGuiElements.xml} excludes the terminal menu, or when the user
clears Enable embedded
+ * terminal. Updated at runtime by {@link #setTerminalsEnabled(boolean)}.
*/
@Getter private boolean terminalsEnabled = true;
+ /** Font-size toolbar items. Present only while {@link #terminalsEnabled} is
true. */
+ private ToolBar dockToolBar;
+
+ private ToolItem increaseFontItem;
+ private ToolItem decreaseFontItem;
+ private ToolItem resetFontItem;
+ private ToolItem fontSeparatorItem;
+
/** Counter used to give each non-singleton tool tab (e.g. a search result
tab) a unique id. */
private int toolTabCounter = 1;
@@ -138,6 +165,23 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
createContents();
}
+ /**
+ * The embedded terminal is on only when the user left it enabled, this is
not Hop Web, and {@code
+ * disabledGuiElements.xml} does not exclude the terminal menu.
+ */
+ public static boolean isTerminalCapabilityEnabled(
+ boolean web, boolean disabledByRegistry, boolean userEnabled) {
+ return userEnabled && !web && !disabledByRegistry;
+ }
+
+ /** Live capability check used by the sidebar and by {@code HopGui} at
startup. */
+ public static boolean isTerminalCapabilityEnabled() {
+ return isTerminalCapabilityEnabled(
+ EnvironmentUtils.getInstance().isWeb(),
+
GuiRegistry.getDisabledGuiElements().contains(ID_MAIN_MENU_TOOLS_TERMINAL),
+ PropsUi.getInstance().isEmbeddedTerminalEnabled());
+ }
+
/** Create the UI structure */
private void createContents() {
setLayout(new FormLayout());
@@ -193,16 +237,7 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
terminalTabs.setLayoutData(fdTabs);
createTerminalToolbar();
-
- // The "+" (new terminal) tab only exists when the terminal capability is
available.
- if (terminalsEnabled) {
- newTerminalTab = new CTabItem(terminalTabs, SWT.NONE);
- newTerminalTab.setText("+");
- newTerminalTab.setToolTipText(
- BaseMessages.getString(PKG, "HopGuiTerminalPanel.NewTab.Tooltip"));
- Composite newTerminalPlaceholder = new Composite(terminalTabs, SWT.NONE);
- newTerminalTab.setControl(newTerminalPlaceholder);
- }
+ createNewTerminalTab();
new TabCloseHandler(this);
new TabFolderReorder(terminalTabs);
@@ -212,42 +247,30 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
SWT.Selection,
event -> {
CTabItem item = terminalTabs.getSelection();
- // When only the "+" tab exists, getSelection() can be null; create
a new terminal.
- if (terminalsEnabled
- && item == null
- && terminalTabs.getItemCount() == 1
- && !isClearing
- && !isClosingTab[0]) {
- createNewTerminal(null, null);
- return;
- }
- if (item == newTerminalTab) {
- // Creation is handled by MouseDown so we don't double-create when
both fire
+ if (item == null || item == newTerminalTab) {
return;
}
-
- if (item != null) {
- ITerminalWidget widget = (ITerminalWidget)
item.getData("terminalWidget");
- if (widget != null) {
- Composite composite = widget.getTerminalComposite();
- if (composite != null && !composite.isDisposed()) {
- composite.forceFocus();
- }
+ lastContentTab = item;
+ ITerminalWidget widget = (ITerminalWidget)
item.getData("terminalWidget");
+ if (widget != null) {
+ Composite composite = widget.getTerminalComposite();
+ if (composite != null && !composite.isDisposed()) {
+ composite.forceFocus();
}
}
+ hopGui.refreshSidebarToolbarButtonStates();
});
- // Ensure + tab click always creates a terminal (e.g. when it's the only
tab and
- // Selection doesn't fire because selection doesn't change)
+ // The "+" tab is not a tool. A click opens a menu of the tabs this panel
can add.
terminalTabs.addListener(
SWT.MouseDown,
event -> {
- if (!terminalsEnabled) {
+ if (isClearing || isClosingTab[0]) {
return;
}
CTabItem item = terminalTabs.getItem(new Point(event.x, event.y));
- if (item == newTerminalTab && !isClearing && !isClosingTab[0]) {
- createNewTerminal(null, null);
+ if (item == newTerminalTab) {
+ showNewTabMenu(new Point(event.x, event.y));
}
});
@@ -288,6 +311,18 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
}
public void createNewTerminal(String workingDirectory, String shellPath,
String customTabName) {
+ createNewTerminal(workingDirectory, shellPath, customTabName, true);
+ }
+
+ /**
+ * @param reveal when true, show the dock, select the new tab, and focus the
terminal. Restore
+ * passes false so a hidden session is recreated without stealing the
current tool tab.
+ */
+ private void createNewTerminal(
+ String workingDirectory, String shellPath, String customTabName, boolean
reveal) {
+ if (!terminalsEnabled) {
+ return;
+ }
if (shellPath == null) {
shellPath = TerminalShellDetector.detectDefaultShell();
}
@@ -296,7 +331,11 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
workingDirectory = getDefaultWorkingDirectory();
}
- CTabItem terminalTab = new CTabItem(terminalTabs, SWT.CLOSE, 1);
+ int insertAt = terminalTabs.getItemCount();
+ if (newTerminalTab != null && !newTerminalTab.isDisposed()) {
+ insertAt = Math.min(1, terminalTabs.getItemCount());
+ }
+ CTabItem terminalTab = new CTabItem(terminalTabs, SWT.CLOSE, insertAt);
String terminalId = "terminal-" + terminalCounter++ + "-" +
System.currentTimeMillis();
@@ -329,18 +368,26 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
registerTerminal(terminalId, workingDirectory, shellPath);
- terminalTabs.setSelection(terminalTab);
-
- if (!terminalVisible) {
- showTerminal();
+ if (reveal) {
+ terminalTabs.setSelection(terminalTab);
+ if (!isDockVisible()) {
+ showDock();
+ }
+ focusTerminalComposite(terminalTab);
}
+ }
+ private void focusTerminalComposite(CTabItem terminalTab) {
+ if (terminalTab == null || terminalTab.isDisposed()) {
+ return;
+ }
+ ITerminalWidget terminalWidget = (ITerminalWidget)
terminalTab.getData("terminalWidget");
+ if (terminalWidget == null) {
+ return;
+ }
getDisplay()
.asyncExec(
() -> {
- if (terminalWidget == null) {
- return;
- }
Composite composite = terminalWidget.getTerminalComposite();
if (composite != null && !composite.isDisposed()) {
composite.setFocus();
@@ -388,7 +435,12 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
}
}
- /** Make the dock visible (without forcing a terminal to be created). */
+ /** True when the bottom panel is on screen. */
+ public boolean isDockVisible() {
+ return terminalVisible;
+ }
+
+ /** Make the dock visible without creating a terminal. */
public void showDock() {
if (!terminalVisible) {
verticalSash.setMaximizedControl(null);
@@ -400,16 +452,17 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
}
}
- /** Show the dock and make sure at least one terminal is present (the Ctrl+J
behaviour). */
- public void showTerminal() {
- showDock();
- if (terminalsEnabled && countTerminalTabs() == 0) {
- createNewTerminal(null, null);
+ /** Show or hide the bottom panel. Does not create a terminal. */
+ public void toggleDock() {
+ if (isDockVisible()) {
+ hideDock();
+ } else {
+ showDock();
}
}
- /** Hide the dock */
- public void hideTerminal() {
+ /** Hide the bottom panel. */
+ public void hideDock() {
if (terminalVisible) {
terminalVisible = false;
verticalSash.setMaximizedControl(perspectiveComposite);
@@ -418,6 +471,126 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
}
}
+ /** Show the dock and make sure at least one terminal is present. */
+ public void showTerminal() {
+ if (!terminalsEnabled) {
+ return;
+ }
+ showDock();
+ if (countTerminalTabs() == 0) {
+ createNewTerminal(null, null);
+ }
+ }
+
+ /** Hide the dock. Kept for callers that still use the terminal name. */
+ public void hideTerminal() {
+ hideDock();
+ }
+
+ /**
+ * Show the dock and focus a terminal tab, creating one when none is open.
Does nothing when the
+ * terminal capability is off.
+ */
+ public void focusTerminal() {
+ if (!terminalsEnabled) {
+ return;
+ }
+ CTabItem terminal = findLastTerminalTab();
+ if (terminal == null) {
+ showTerminal();
+ return;
+ }
+ selectTab(terminal);
+ focusTerminalComposite(terminal);
+ }
+
+ /**
+ * Id of the selected tab: {@link #TOOL_ID_TERMINAL} for a terminal, the
tool id for a tool tab,
+ * or null for the "+" tab and an empty selection.
+ */
+ public String getSelectedToolId() {
+ if (terminalTabs == null || terminalTabs.isDisposed()) {
+ return null;
+ }
+ CTabItem selected = terminalTabs.getSelection();
+ if (selected == null || selected == newTerminalTab ||
selected.isDisposed()) {
+ return null;
+ }
+ if (selected.getData("terminalWidget") != null) {
+ return TOOL_ID_TERMINAL;
+ }
+ Object toolId = selected.getData(DATA_TOOL_ID);
+ return toolId instanceof String id ? id : null;
+ }
+
+ /**
+ * True when {@code toolId} is the selected tab. A value of {@link
#SEARCH_TOOL_ID_PREFIX} matches
+ * any search-results tab.
+ */
+ public boolean isToolSelected(String toolId) {
+ String selected = getSelectedToolId();
+ if (selected == null || toolId == null) {
+ return false;
+ }
+ if (SEARCH_TOOL_ID_PREFIX.equals(toolId)) {
+ return selected.startsWith(SEARCH_TOOL_ID_PREFIX);
+ }
+ return toolId.equals(selected);
+ }
+
+ /** Select a tab and make sure the dock is visible. */
+ public void selectTab(CTabItem item) {
+ if (item == null || item.isDisposed()) {
+ return;
+ }
+ showDock();
+ terminalTabs.setSelection(item);
+ hopGui.refreshSidebarToolbarButtonStates();
+ }
+
+ /** Content control stored on a tool tab, or null. */
+ public Control getToolContent(CTabItem item) {
+ if (item == null || item.isDisposed()) {
+ return null;
+ }
+ Object content = item.getData(DATA_TOOL_CONTENT);
+ return content instanceof Control control ? control : null;
+ }
+
+ /** Last open terminal tab, or null. */
+ public CTabItem findLastTerminalTab() {
+ CTabItem last = null;
+ if (terminalTabs == null || terminalTabs.isDisposed()) {
+ return null;
+ }
+ for (CTabItem item : terminalTabs.getItems()) {
+ if (item != newTerminalTab && item.getData("terminalWidget") != null) {
+ last = item;
+ }
+ }
+ return last;
+ }
+
+ /** Last tool tab whose id starts with {@code prefix}, or null. */
+ public CTabItem findLastToolTabByPrefix(String prefix) {
+ if (prefix == null || terminalTabs == null || terminalTabs.isDisposed()) {
+ return null;
+ }
+ CTabItem last = null;
+ for (CTabItem item : terminalTabs.getItems()) {
+ Object id = item.getData(DATA_TOOL_ID);
+ if (id instanceof String toolId && toolId.startsWith(prefix)) {
+ last = item;
+ }
+ }
+ return last;
+ }
+
+ /** Next id for a search-results tab ({@code search-1}, {@code search-2},
...). */
+ public String nextSearchToolId() {
+ return SEARCH_TOOL_ID_PREFIX + (toolTabCounter++);
+ }
+
/** Number of real terminal tabs currently open (excludes the "+" tab and
any tool tabs). */
private int countTerminalTabs() {
int count = 0;
@@ -470,7 +643,21 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
*/
public Control openToolTab(
String title, Image image, boolean closable, Function<Composite,
Control> contentFactory) {
- return createToolTab("tool-" + (toolTabCounter++), title, image, closable,
contentFactory);
+ return openToolTab(null, title, image, closable, contentFactory);
+ }
+
+ /**
+ * Always open a new tool tab. {@code toolId} is stored on the tab so it can
be found again; a
+ * blank id gets a generated {@code tool-} id.
+ */
+ public Control openToolTab(
+ String toolId,
+ String title,
+ Image image,
+ boolean closable,
+ Function<Composite, Control> contentFactory) {
+ String id = StringUtils.isEmpty(toolId) ? "tool-" + (toolTabCounter++) :
toolId;
+ return createToolTab(id, title, image, closable, contentFactory);
}
private Control createToolTab(
@@ -520,7 +707,10 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
return null;
}
- /** Toggle terminal panel visibility */
+ /**
+ * Show the terminal, or hide the panel when a terminal tab is already
selected. Does nothing when
+ * the terminal capability is off.
+ */
@GuiMenuElement(
root = HopGui.ID_MAIN_MENU,
id = ID_MAIN_MENU_TOOLS_TERMINAL,
@@ -532,10 +722,10 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
if (EnvironmentUtils.getInstance().isWeb() || !terminalsEnabled) {
return;
}
- if (terminalVisible) {
- hideTerminal();
+ if (isDockVisible() && isToolSelected(TOOL_ID_TERMINAL)) {
+ hideDock();
} else {
- showTerminal();
+ focusTerminal();
}
}
@@ -584,6 +774,7 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
}
tabItem.dispose();
+ hopGui.refreshSidebarToolbarButtonStates();
}
/** Get the terminal tabs folder (implements TabClosable interface) */
@@ -635,47 +826,21 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
/** Create toolbar with font size controls and panel controls
(maximize/minimize, close) */
private void createTerminalToolbar() {
- ToolBar toolBar = new ToolBar(terminalTabs, SWT.FLAT);
- terminalTabs.setTopRight(toolBar, SWT.RIGHT);
- PropsUi.setLook(toolBar);
+ dockToolBar = new ToolBar(terminalTabs, SWT.FLAT);
+ terminalTabs.setTopRight(dockToolBar, SWT.RIGHT);
+ PropsUi.setLook(dockToolBar);
GuiResource gui = GuiResource.getInstance();
if (PropsUi.getInstance().isDarkMode()) {
- toolBar.setBackground(gui.getColorWhite());
+ dockToolBar.setBackground(gui.getColorWhite());
} else {
- toolBar.setBackground(terminalTabs.getBackground());
+ dockToolBar.setBackground(terminalTabs.getBackground());
}
- // Font sizing only applies to terminal tabs, so the controls are
terminal-gated.
- if (terminalsEnabled) {
- // Font size: increase
- ToolItem increaseFontItem = new ToolItem(toolBar, SWT.PUSH);
- increaseFontItem.setImage(
- GuiResource.getInstance().getImage("ui/images/zoom-in.svg", 16, 16));
- increaseFontItem.setToolTipText(
- BaseMessages.getString(PKG,
"HopGuiTerminalPanel.Toolbar.IncreaseFont"));
- increaseFontItem.addListener(SWT.Selection, e ->
increaseTerminalFontSize());
-
- // Font size: decrease
- ToolItem decreaseFontItem = new ToolItem(toolBar, SWT.PUSH);
- decreaseFontItem.setImage(
- GuiResource.getInstance().getImage("ui/images/zoom-out.svg", 16,
16));
- decreaseFontItem.setToolTipText(
- BaseMessages.getString(PKG,
"HopGuiTerminalPanel.Toolbar.DecreaseFont"));
- decreaseFontItem.addListener(SWT.Selection, e ->
decreaseTerminalFontSize());
-
- // Font size: reset to 100%
- ToolItem resetFontItem = new ToolItem(toolBar, SWT.PUSH);
-
resetFontItem.setImage(GuiResource.getInstance().getImage("ui/images/zoom-100.svg",
16, 16));
- resetFontItem.setToolTipText(
- BaseMessages.getString(PKG,
"HopGuiTerminalPanel.Toolbar.ResetFont"));
- resetFontItem.addListener(SWT.Selection, e -> resetTerminalFontSize());
-
- new ToolItem(toolBar, SWT.SEPARATOR);
- }
+ createFontToolItems();
// Maximize/Minimize button
- final ToolItem maximizeItem = new ToolItem(toolBar, SWT.PUSH);
+ final ToolItem maximizeItem = new ToolItem(dockToolBar, SWT.PUSH);
maximizeItem.setImage(GuiResource.getInstance().getImageMaximizePanel());
maximizeItem.setToolTipText(
BaseMessages.getString(PKG, "HopGuiTerminalPanel.Toolbar.Maximize"));
@@ -683,7 +848,7 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
SWT.Selection,
e -> {
if (verticalSash.getMaximizedControl() == null) {
- // Maximize terminal panel
+ // Maximize the bottom panel
verticalSash.setMaximizedControl(bottomPanelComposite);
maximizeItem.setImage(GuiResource.getInstance().getImageMinimizePanel());
maximizeItem.setToolTipText(
@@ -699,15 +864,226 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
});
// Close button
- final ToolItem closeItem = new ToolItem(toolBar, SWT.PUSH);
+ final ToolItem closeItem = new ToolItem(dockToolBar, SWT.PUSH);
closeItem.setImage(GuiResource.getInstance().getImageClose());
closeItem.setToolTipText(BaseMessages.getString(PKG,
"HopGuiTerminalPanel.Toolbar.Close"));
- closeItem.addListener(SWT.Selection, e -> hideTerminal());
+ closeItem.addListener(SWT.Selection, e -> hideDock());
- int height = toolBar.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
+ int height = dockToolBar.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
terminalTabs.setTabHeight(Math.max(height, terminalTabs.getTabHeight()));
}
+ /** Font-size controls sit in front of maximize and close, and only while
the terminal is on. */
+ private void createFontToolItems() {
+ if (!terminalsEnabled || dockToolBar == null || dockToolBar.isDisposed()) {
+ return;
+ }
+ if (increaseFontItem != null && !increaseFontItem.isDisposed()) {
+ return;
+ }
+ increaseFontItem = new ToolItem(dockToolBar, SWT.PUSH, 0);
+
increaseFontItem.setImage(GuiResource.getInstance().getImage("ui/images/zoom-in.svg",
16, 16));
+ increaseFontItem.setToolTipText(
+ BaseMessages.getString(PKG,
"HopGuiTerminalPanel.Toolbar.IncreaseFont"));
+ increaseFontItem.addListener(SWT.Selection, e ->
increaseTerminalFontSize());
+
+ decreaseFontItem = new ToolItem(dockToolBar, SWT.PUSH, 1);
+
decreaseFontItem.setImage(GuiResource.getInstance().getImage("ui/images/zoom-out.svg",
16, 16));
+ decreaseFontItem.setToolTipText(
+ BaseMessages.getString(PKG,
"HopGuiTerminalPanel.Toolbar.DecreaseFont"));
+ decreaseFontItem.addListener(SWT.Selection, e ->
decreaseTerminalFontSize());
+
+ resetFontItem = new ToolItem(dockToolBar, SWT.PUSH, 2);
+
resetFontItem.setImage(GuiResource.getInstance().getImage("ui/images/zoom-100.svg",
16, 16));
+ resetFontItem.setToolTipText(
+ BaseMessages.getString(PKG, "HopGuiTerminalPanel.Toolbar.ResetFont"));
+ resetFontItem.addListener(SWT.Selection, e -> resetTerminalFontSize());
+
+ fontSeparatorItem = new ToolItem(dockToolBar, SWT.SEPARATOR, 3);
+ }
+
+ private void disposeFontToolItems() {
+ disposeToolItem(fontSeparatorItem);
+ disposeToolItem(resetFontItem);
+ disposeToolItem(decreaseFontItem);
+ disposeToolItem(increaseFontItem);
+ fontSeparatorItem = null;
+ resetFontItem = null;
+ decreaseFontItem = null;
+ increaseFontItem = null;
+ }
+
+ private static void disposeToolItem(ToolItem item) {
+ if (item != null && !item.isDisposed()) {
+ item.dispose();
+ }
+ }
+
+ /** The "+" tab is always present. Choosing an entry adds that tool without
hiding the panel. */
+ private void showNewTabMenu(Point locationInFolder) {
+ CTabItem current = terminalTabs.getSelection();
+ if (current != null && current != newTerminalTab && !current.isDisposed())
{
+ lastContentTab = current;
+ }
+ Menu menu = new Menu(terminalTabs);
+ if (terminalsEnabled) {
+ addNewTabMenuItem(
+ menu,
+ BaseMessages.getString(PKG, "HopGuiTerminalPanel.NewTab.Terminal"),
+ GuiResource.getInstance().getImageTerminal(),
+ () -> createNewTerminal(null, null));
+ }
+ addNewTabMenuItem(
+ menu,
+ BaseMessages.getString(PKG, "HopGuiTerminalPanel.NewTab.Search"),
+ GuiResource.getInstance().getImageSearch(),
+ this::openNewSearchTab);
+ addNewTabMenuItem(
+ menu,
+ BaseMessages.getString(PKG, "HopGuiTerminalPanel.NewTab.Database"),
+ GuiResource.getInstance().getImageDatabase(),
+ () -> DatabaseWorkbenchViews.openDock(hopGui));
+ addNewTabMenuItem(
+ menu,
+ BaseMessages.getString(PKG, "HopGuiTerminalPanel.NewTab.Vfs"),
+ GuiResource.getInstance().getImageFolder(),
+ () -> VfsFileExplorerViews.openOrFocusDock(hopGui));
+ CTabItem restore = lastContentTab;
+ menu.addListener(
+ SWT.Hide,
+ event ->
+ getDisplay()
+ .asyncExec(
+ () -> {
+ if (!terminalTabs.isDisposed()
+ && (terminalTabs.getSelection() == null
+ || terminalTabs.getSelection() == newTerminalTab)
+ && restore != null
+ && !restore.isDisposed()) {
+ terminalTabs.setSelection(restore);
+ }
+ if (!menu.isDisposed()) {
+ menu.dispose();
+ }
+ }));
+ Point displayPoint = terminalTabs.toDisplay(locationInFolder);
+ menu.setLocation(displayPoint);
+ menu.setVisible(true);
+ }
+
+ private void addNewTabMenuItem(Menu menu, String text, Image image, Runnable
action) {
+ MenuItem item = new MenuItem(menu, SWT.PUSH);
+ item.setText(text);
+ if (image != null && !image.isDisposed()) {
+ item.setImage(image);
+ }
+ item.addListener(SWT.Selection, event -> action.run());
+ }
+
+ /** A new search-results tab. Search can keep several queries open next to
each other. */
+ private void openNewSearchTab() {
+ String title =
+ BaseMessages.getString(
+ SearchEverywhereDialog.class,
"SearchEverywhereDialog.ShowAll.TabTitle");
+ Control content =
+ openToolTab(
+ nextSearchToolId(),
+ title,
+ GuiResource.getInstance().getImageSearch(),
+ true,
+ container -> new HopGuiSearchResultsPanel(container, hopGui));
+ if (content instanceof HopGuiSearchResultsPanel panel) {
+ panel.focusSearchField();
+ }
+ }
+
+ private void createNewTerminalTab() {
+ if (newTerminalTab != null && !newTerminalTab.isDisposed()) {
+ return;
+ }
+ newTerminalTab = new CTabItem(terminalTabs, SWT.NONE, 0);
+ newTerminalTab.setText("+");
+ newTerminalTab.setToolTipText(
+ BaseMessages.getString(PKG, "HopGuiTerminalPanel.NewTab.Tooltip"));
+ Composite placeholder = new Composite(terminalTabs, SWT.NONE);
+ newTerminalTab.setControl(placeholder);
+ }
+
+ /**
+ * Turn the embedded terminal on or off without restarting. Turning it off
closes PTY tabs and
+ * drops the font controls. The "+" tab stays, without a Terminal entry.
Turning the terminal on
+ * puts the font controls back and does not open a shell.
+ */
+ public void setTerminalsEnabled(boolean enabled) {
+ if (this.terminalsEnabled == enabled) {
+ return;
+ }
+ this.terminalsEnabled = enabled;
+ if (!enabled) {
+ // Save first. Disposing the tabs must not write an empty list over the
open sessions, so a
+ // later restore (after the user turns the terminal back on) can
recreate them.
+ saveOpenTerminals();
+ disposeTerminalTabs();
+ disposeFontToolItems();
+ if (!hasContentTabs()) {
+ hideDock();
+ }
+ } else {
+ createFontToolItems();
+ }
+ }
+
+ /**
+ * Drop terminal tabs and their PTY widgets without updating the saved
terminal list. {@link
+ * #closeTab} is the path that forgets a tab the user closed.
+ */
+ private void disposeTerminalTabs() {
+ java.util.List<CTabItem> itemsToClose = new java.util.ArrayList<>();
+ for (CTabItem item : terminalTabs.getItems()) {
+ if (!item.isDisposed() && item.getData("terminalWidget") != null) {
+ itemsToClose.add(item);
+ }
+ }
+ for (CTabItem item : itemsToClose) {
+ disposeTerminalTab(item);
+ }
+ hopGui.refreshSidebarToolbarButtonStates();
+ }
+
+ private void disposeTerminalTab(CTabItem item) {
+ if (item == null || item.isDisposed()) {
+ return;
+ }
+ ITerminalWidget widget = (ITerminalWidget) item.getData("terminalWidget");
+ if (widget != null) {
+ widget.dispose();
+ }
+ Control tabControl = item.getControl();
+ item.dispose();
+ if (tabControl != null && !tabControl.isDisposed()) {
+ tabControl.dispose();
+ }
+ }
+
+ /** True when a tab other than "+" is open (a terminal or a tool). */
+ private boolean hasContentTabs() {
+ for (CTabItem item : terminalTabs.getItems()) {
+ if (item != newTerminalTab && !item.isDisposed()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasToolTabs() {
+ for (CTabItem item : terminalTabs.getItems()) {
+ if (!item.isDisposed() && item.getData(DATA_TOOL_ID) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void increaseTerminalFontSize() {
terminalFontSizePercent = Math.min(200, terminalFontSizePercent + 10);
applyFontSizeToAllTerminals();
@@ -864,27 +1240,12 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
isClearing = true;
try {
+ // Persist this project's terminals before the tabs go away.
disposeTerminalTabs does not
+ // write the audit, so the list survives for the next visit to this
project.
saveOpenTerminals();
-
- java.util.List<CTabItem> itemsToClose = new java.util.ArrayList<>();
- for (CTabItem item : terminalTabs.getItems()) {
- if (item != newTerminalTab && !item.isDisposed()) {
- itemsToClose.add(item);
- }
- }
-
- for (CTabItem item : itemsToClose) {
- if (!item.isDisposed()) {
- ITerminalWidget widget = (ITerminalWidget)
item.getData("terminalWidget");
- if (widget != null) {
- widget.dispose();
- }
- item.dispose();
- }
- }
-
- if (terminalVisible) {
- hideTerminal();
+ disposeTerminalTabs();
+ if (!hasContentTabs()) {
+ hideDock();
}
} finally {
isClearing = false;
@@ -923,14 +1284,9 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
try {
String namespace = HopNamespace.getNamespace();
- int existingCount = 0;
- for (CTabItem item : terminalTabs.getItems()) {
- if (item != newTerminalTab) {
- existingCount++;
- }
- }
-
- if (existingCount > 0) {
+ // Tool tabs (search, database, VFS, AI) stay open across a project
switch and must not block
+ // restoring this project's terminals.
+ if (countTerminalTabs() > 0) {
return;
}
@@ -980,14 +1336,16 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
return;
}
- // If panel was hidden when saved, create terminals without showing the
panel
- boolean wasVisible = terminalVisible;
- if (!savedPanelVisible) {
- terminalVisible = true; // prevent createNewTerminal from calling
showTerminal()
- }
+ // A tool tab the user is looking at stays put when this project last
had the panel hidden.
+ // Otherwise a visible save selects the restored terminal, and a hidden
save with no tool tab
+ // keeps the shells off screen.
+ boolean keepToolTab = !savedPanelVisible && hasToolTabs() &&
isDockVisible();
+ boolean reveal = savedPanelVisible;
for (String terminalId : auditList.getNames()) {
- if (STATE_PANEL_VISIBLE_KEY.equals(terminalId)) {
+ if (STATE_PANEL_VISIBLE_KEY.equals(terminalId)
+ || STATE_TERMINAL_FONT_SIZE_PERCENT_KEY.equals(terminalId)
+ || STATE_TERMINAL_HEIGHT_PERCENT_KEY.equals(terminalId)) {
continue;
}
String customTabName = null;
@@ -1010,12 +1368,11 @@ public class HopGuiBottomDock extends Composite
implements TabClosable {
}
}
- createNewTerminal(workingDir, shellPath, customTabName);
+ createNewTerminal(workingDir, shellPath, customTabName, reveal);
}
- if (!savedPanelVisible) {
- terminalVisible = wasVisible;
- hideTerminal();
+ if (!savedPanelVisible && !keepToolTab) {
+ hideDock();
}
} catch (Exception e) {
hopGui
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerViews.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerViews.java
index 51d744c1f6..50640161c4 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerViews.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerViews.java
@@ -168,7 +168,48 @@ public class VfsFileExplorerViews {
if (!isDockOpen(hopGui)) {
return false;
}
- return hopGui.getTerminalPanel().isTerminalVisible();
+ return hopGui.getTerminalPanel().isDockVisible();
+ }
+
+ /**
+ * Show the explorer in the bottom panel. An explorer that is already open
keeps its location. The
+ * first open uses the project home, or the user home when that is not set.
+ */
+ public static void openOrFocusDock(HopGui hopGui) {
+ if (hopGui == null) {
+ return;
+ }
+ HopGuiBottomDock dock = hopGui.getTerminalPanel();
+ if (dock == null || dock.isDisposed()) {
+ return;
+ }
+ if (isDockOpen(hopGui)) {
+ CTabItem item = dock.findToolTab(DOCK_TOOL_ID);
+ dock.selectTab(item);
+ Control control = dock.getToolContent(item);
+ if (control instanceof VfsFileExplorer explorer &&
!explorer.isDisposed()) {
+ explorer.activate();
+ }
+ return;
+ }
+ openDock(hopGui, defaultDockLocation(hopGui));
+ }
+
+ private static String defaultDockLocation(HopGui hopGui) {
+ try {
+ if (hopGui.getVariables() != null) {
+ String projectHome = hopGui.getVariables().getVariable("PROJECT_HOME");
+ if (StringUtils.isNotEmpty(projectHome)) {
+ projectHome = hopGui.getVariables().resolve(projectHome);
+ if (StringUtils.isNotEmpty(projectHome)) {
+ return projectHome;
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Fall back to the user home.
+ }
+ return System.getProperty("user.home");
}
@GuiMenuElement(
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 e6ce3c9167..cf8ecde5a4 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
@@ -185,6 +185,9 @@ EnterOptionsDialog.KeyboardShortuts=Keyboard Shortcuts
EnterOptionsDialog.MaxExecutionLoggingTextSizeSize.Label=Maximum execution
logging text size:
EnterOptionsDialog.MaxExecutionLoggingTextSizeSize.ToolTip=The maximum size of
the log text shown in the execution information perspective to prevent out of
memory errors.
EnterOptionsDialog.OpenLastFileStartup.Label=Reopen tabs on startup
+EnterOptionsDialog.EmbeddedTerminal.Label=Enable embedded terminal
+EnterOptionsDialog.EmbeddedTerminal.ToolTip=Turn this off on desktops where
the built-in terminal misbehaves. Search, Database, and VFS File Explorer in
the bottom panel stay available.
+EnterOptionsDialog.EmbeddedTerminal.ForcedOff.ToolTip=The embedded terminal is
disabled in disabledGuiElements.xml and cannot be turned on here.
EnterOptionsDialog.ReloadFileOnChange.Label=Reload file if changed on
filesystem
EnterOptionsDialog.ReloadFileOnChange.ToolTip=Automatically reload files when
they are changed outside of Apache Hop
EnterOptionsDialog.RestartWarning.DialogMessage=Please restart the Hop GUI to
apply look and feel changes
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 bc02ac1e94..67cdf0f13c 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
@@ -143,6 +143,7 @@ HopGui.Menu.View=&View
HopGui.Menu.View.FullScreen=Full screen
HopGui.Menu.View.Terminal=&Terminal
HopGui.Menu.View.NewTerminal=&New Terminal
+HopGui.Sidebar.BottomPanel.Tooltip=Show or hide the bottom panel
HopGui.Menu.Redo.Available=Redo \: {0}
HopGui.Menu.Redo.NotAvailable=Redo \: not available
HopGui.Menu.Run=&Run
diff --git
a/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/messages/messages_en_US.properties
index 75b9b89cfc..ef438c38b6 100644
---
a/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/messages/messages_en_US.properties
@@ -21,7 +21,11 @@ HopGuiTerminalPanel.Menu.NewTerminal=New Terminal
HopGuiTerminalPanel.Toolbar.IncreaseFont=Increase font size
HopGuiTerminalPanel.Toolbar.DecreaseFont=Decrease font size
HopGuiTerminalPanel.Toolbar.ResetFont=Reset font to 100%
-HopGuiTerminalPanel.NewTab.Tooltip=Create a new terminal
+HopGuiTerminalPanel.NewTab.Tooltip=Open a new tab
+HopGuiTerminalPanel.NewTab.Terminal=Terminal
+HopGuiTerminalPanel.NewTab.Search=Search
+HopGuiTerminalPanel.NewTab.Database=Database
+HopGuiTerminalPanel.NewTab.Vfs=VFS File Explorer
HopGuiTerminalPanel.Tab.Tooltip=Terminal: {0} in {1}
HopGuiTerminalPanel.ShellName.Default=Terminal
HopGuiTerminalPanel.Toolbar.Maximize=Maximize terminal panel
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDockTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDockTest.java
index 3b86bb96d0..889aef8c56 100644
---
a/ui/src/test/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDockTest.java
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDockTest.java
@@ -179,4 +179,13 @@ class HopGuiBottomDockTest {
assertNotNull(defaultWorkingDir, "User home should not be null");
assertFalse(defaultWorkingDir.isEmpty(), "User home should not be empty");
}
+
+ @Test
+ void terminalCapabilityRequiresTheUserOptionOnTheDesktop() {
+ assertTrue(HopGuiBottomDock.isTerminalCapabilityEnabled(false, false,
true));
+ assertFalse(HopGuiBottomDock.isTerminalCapabilityEnabled(false, false,
false));
+ assertFalse(HopGuiBottomDock.isTerminalCapabilityEnabled(true, false,
true));
+ assertFalse(HopGuiBottomDock.isTerminalCapabilityEnabled(false, true,
true));
+ assertFalse(HopGuiBottomDock.isTerminalCapabilityEnabled(true, true,
false));
+ }
}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerViewsTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerViewsTest.java
new file mode 100644
index 0000000000..0839249d6f
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerViewsTest.java
@@ -0,0 +1,33 @@
+/*
+ * 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.vfs.explorer;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.junit.jupiter.api.Test;
+
+class VfsFileExplorerViewsTest {
+
+ @Test
+ void openOrFocusDockToleratesNullHopGui() {
+ assertDoesNotThrow(() -> VfsFileExplorerViews.openOrFocusDock(null));
+ assertFalse(VfsFileExplorerViews.isExplorerDockVisible(null));
+ assertFalse(VfsFileExplorerViews.isDockOpen(null));
+ }
+}