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 6f25de691d fix keyboard shortcuts, fixes #6613 (#6618)
6f25de691d is described below

commit 6f25de691dbd4e0c17429801dc0b7d2f48e60f40
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Feb 19 18:01:51 2026 +0100

    fix keyboard shortcuts, fixes #6613 (#6618)
    
    fix header
---
 .../apache/hop/core/gui/plugin/GuiRegistry.java    |  21 ++-
 .../core/gui/plugin/key/GuiKeyboardShortcut.java   |   3 +
 .../gui/plugin/key/GuiOsxKeyboardShortcut.java     |   3 +
 .../hop/core/gui/plugin/key/KeyboardShortcut.java  |  34 ++++
 .../main/java/org/apache/hop/git/GitGuiPlugin.java |   4 -
 .../apache/hop/imports/gui/HopImportGuiPlugin.java |   6 +-
 .../org/apache/hop/ui/core/gui/BaseGuiWidgets.java |   2 +
 .../org/apache/hop/ui/core/gui/GuiMenuWidgets.java | 100 ++++-------
 .../apache/hop/ui/core/gui/GuiToolbarWidgets.java  |  10 +-
 .../hop/ui/core/gui/ShortcutDisplayUtil.java       | 166 +++++++++++++++++
 .../apache/hop/ui/core/vfs/HopVfsFileDialog.java   |  10 +-
 .../main/java/org/apache/hop/ui/hopgui/HopGui.java |  57 ++----
 .../org/apache/hop/ui/hopgui/HopGuiKeyHandler.java | 143 +++++++++++----
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  |  45 ++---
 .../delegates/HopGuiPipelineGridDelegate.java      |  27 +--
 .../hopgui/file/workflow/HopGuiWorkflowGraph.java  |  31 +---
 .../configuration/ConfigurationPerspective.java    |  14 +-
 .../tabs/ConfigKeyboardShortcutsTab.java           | 200 +++------------------
 .../execution/ExecutionPerspective.java            |   4 +-
 .../perspective/explorer/ExplorerPerspective.java  |  15 +-
 .../perspective/metadata/MetadataPerspective.java  |   4 +-
 .../perspective/search/HopSearchPerspective.java   |   4 +-
 .../hopgui/selection/HopGuiSelectionTracker.java   | 107 -----------
 .../ui/hopgui/terminal/HopGuiTerminalPanel.java    |  76 +++++---
 .../terminal/messages/messages_en_US.properties    |  20 +++
 25 files changed, 548 insertions(+), 558 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiRegistry.java 
b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiRegistry.java
index 5a43478879..de98795e97 100644
--- a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiRegistry.java
+++ b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiRegistry.java
@@ -492,8 +492,27 @@ public class GuiRegistry {
     shortcuts.add(new KeyboardShortcut(shortcut, parentMethod));
   }
 
+  /**
+   * Returns keyboard shortcuts for the given class in a stable order: by 
method name, then by
+   * isOsx() (non-OSX before OSX). Callers (menus, config panel, key handler) 
then see a consistent
+   * modifier/key order and display order.
+   */
   public List<KeyboardShortcut> getKeyboardShortcuts(String parentClassName) {
-    return shortCutsMap.get(parentClassName);
+    List<KeyboardShortcut> list = shortCutsMap.get(parentClassName);
+    if (list == null) {
+      return null;
+    }
+    if (list.isEmpty()) {
+      return list;
+    }
+    List<KeyboardShortcut> sorted = new ArrayList<>(list);
+    sorted.sort(
+        (a, b) -> {
+          int c = a.getParentMethodName().compareTo(b.getParentMethodName());
+          if (c != 0) return c;
+          return Boolean.compare(a.isOsx(), b.isOsx());
+        });
+    return sorted;
   }
 
   /**
diff --git 
a/core/src/main/java/org/apache/hop/core/gui/plugin/key/GuiKeyboardShortcut.java
 
b/core/src/main/java/org/apache/hop/core/gui/plugin/key/GuiKeyboardShortcut.java
index 8b5e61dda7..a1cc09d90d 100644
--- 
a/core/src/main/java/org/apache/hop/core/gui/plugin/key/GuiKeyboardShortcut.java
+++ 
b/core/src/main/java/org/apache/hop/core/gui/plugin/key/GuiKeyboardShortcut.java
@@ -39,4 +39,7 @@ public @interface GuiKeyboardShortcut {
   boolean command() default false;
 
   int key() default -1;
+
+  /** If true, shortcut works from anywhere (e.g. perspective activate, toggle 
terminal). */
+  boolean global() default false;
 }
diff --git 
a/core/src/main/java/org/apache/hop/core/gui/plugin/key/GuiOsxKeyboardShortcut.java
 
b/core/src/main/java/org/apache/hop/core/gui/plugin/key/GuiOsxKeyboardShortcut.java
index bbc0465e6b..53fcbe2a3f 100644
--- 
a/core/src/main/java/org/apache/hop/core/gui/plugin/key/GuiOsxKeyboardShortcut.java
+++ 
b/core/src/main/java/org/apache/hop/core/gui/plugin/key/GuiOsxKeyboardShortcut.java
@@ -39,4 +39,7 @@ public @interface GuiOsxKeyboardShortcut {
   boolean command() default false;
 
   int key() default -1;
+
+  /** If true, shortcut works from anywhere (e.g. perspective activate, toggle 
terminal). */
+  boolean global() default false;
 }
diff --git 
a/core/src/main/java/org/apache/hop/core/gui/plugin/key/KeyboardShortcut.java 
b/core/src/main/java/org/apache/hop/core/gui/plugin/key/KeyboardShortcut.java
index 7e7a699fe3..e876b272dd 100644
--- 
a/core/src/main/java/org/apache/hop/core/gui/plugin/key/KeyboardShortcut.java
+++ 
b/core/src/main/java/org/apache/hop/core/gui/plugin/key/KeyboardShortcut.java
@@ -28,6 +28,12 @@ public class KeyboardShortcut {
   private boolean command;
   private int keyCode;
 
+  /** If true, shortcut works from anywhere (e.g. activate perspective, toggle 
terminal). */
+  private boolean global;
+
+  /** Class that declares the shortcut (where it should be triggered). */
+  private String parentClassName;
+
   private String parentMethodName;
 
   public KeyboardShortcut() {
@@ -41,6 +47,8 @@ public class KeyboardShortcut {
     this.shift = shortcut.shift();
     this.command = shortcut.command();
     this.keyCode = shortcut.key();
+    this.global = shortcut.global();
+    this.parentClassName = parentMethod.getDeclaringClass().getName();
     this.parentMethodName = parentMethod.getName();
   }
 
@@ -51,6 +59,8 @@ public class KeyboardShortcut {
     this.shift = shortcut.shift();
     this.command = shortcut.command();
     this.keyCode = shortcut.key();
+    this.global = shortcut.global();
+    this.parentClassName = parentMethod.getDeclaringClass().getName();
     this.parentMethodName = parentMethod.getName();
   }
 
@@ -305,6 +315,22 @@ public class KeyboardShortcut {
     this.keyCode = keyCode;
   }
 
+  /**
+   * Gets parentClassName (class where this shortcut should be triggered).
+   *
+   * @return value of parentClassName
+   */
+  public String getParentClassName() {
+    return parentClassName;
+  }
+
+  /**
+   * @param parentClassName The parentClassName to set
+   */
+  public void setParentClassName(String parentClassName) {
+    this.parentClassName = parentClassName;
+  }
+
   /**
    * Gets parentMethodName
    *
@@ -320,4 +346,12 @@ public class KeyboardShortcut {
   public void setParentMethodName(String parentMethodName) {
     this.parentMethodName = parentMethodName;
   }
+
+  public boolean isGlobal() {
+    return global;
+  }
+
+  public void setGlobal(boolean global) {
+    this.global = global;
+  }
 }
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java 
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
index 1f5434bfac..5540edea47 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
@@ -32,8 +32,6 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopFileException;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.callback.GuiCallback;
-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;
 import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
 import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElementType;
@@ -318,8 +316,6 @@ public class GitGuiPlugin
       id = TOOLBAR_ITEM_PULL,
       toolTip = "i18n::GitGuiPlugin.Toolbar.Pull.Tooltip",
       image = "pull.svg")
-  @GuiKeyboardShortcut(control = true, key = 'T')
-  @GuiOsxKeyboardShortcut(control = true, key = 'T')
   public void gitPull() {
     try {
       if (git.pull()) {
diff --git 
a/plugins/misc/import/src/main/java/org/apache/hop/imports/gui/HopImportGuiPlugin.java
 
b/plugins/misc/import/src/main/java/org/apache/hop/imports/gui/HopImportGuiPlugin.java
index 12784ab21c..3538c44e1a 100644
--- 
a/plugins/misc/import/src/main/java/org/apache/hop/imports/gui/HopImportGuiPlugin.java
+++ 
b/plugins/misc/import/src/main/java/org/apache/hop/imports/gui/HopImportGuiPlugin.java
@@ -26,7 +26,7 @@ import org.apache.hop.imports.kettle.KettleImportDialog;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.hopgui.HopGui;
 
-@GuiPlugin
+@GuiPlugin(name = "Import", description = "Import Kettle projects")
 public class HopImportGuiPlugin {
 
   public static final String ID_MAIN_MENU_FILE_IMPORT = 
"10060-menu-tools-import";
@@ -47,8 +47,8 @@ public class HopImportGuiPlugin {
       image = "kettle-logo.svg",
       parentId = HopGui.ID_MAIN_MENU_FILE,
       separator = true)
-  @GuiKeyboardShortcut(control = true, key = 'i')
-  @GuiOsxKeyboardShortcut(command = true, key = 'i')
+  @GuiKeyboardShortcut(control = true, key = 'i', global = true)
+  @GuiOsxKeyboardShortcut(command = true, key = 'i', global = true)
   public void menuToolsImport() {
     HopGui hopGui = HopGui.getInstance();
     try {
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/BaseGuiWidgets.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/BaseGuiWidgets.java
index ebe7265b4b..d6acd05542 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/BaseGuiWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/BaseGuiWidgets.java
@@ -27,6 +27,7 @@ import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.HopGuiKeyHandler;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Listener;
 
@@ -98,6 +99,7 @@ public class BaseGuiWidgets {
         GuiRegistry.getInstance()
             .registerGuiPluginObject(hopGuiId, listenerClassName, instanceId, 
guiPluginObject);
       }
+      HopGuiKeyHandler.getInstance().addParentObjectToHandle(guiPluginObject);
       return guiPluginObject;
     } catch (Exception e) {
       throw new HopException(
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
index 17cdd43d43..9181fe33d2 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
@@ -25,10 +25,13 @@ import java.util.Map;
 import java.util.UUID;
 import org.apache.commons.lang.StringUtils;
 import org.apache.hop.core.Const;
+import org.apache.hop.core.gui.plugin.GuiPluginType;
 import org.apache.hop.core.gui.plugin.GuiRegistry;
 import org.apache.hop.core.gui.plugin.key.KeyboardShortcut;
 import org.apache.hop.core.gui.plugin.menu.GuiMenuItem;
 import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
 import org.apache.hop.ui.core.ConstUi;
 import org.apache.hop.ui.hopgui.file.IHopFileType;
 import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
@@ -52,6 +55,31 @@ public class GuiMenuWidgets extends BaseGuiWidgets {
     this.menuEnabledMap = new HashMap<>();
   }
 
+  /** Pre-create shortcut plugin instances so their shortcuts work before menu 
is used. */
+  public void ensureShortcutPluginInstancesRegistered() {
+    GuiRegistry guiRegistry = GuiRegistry.getInstance();
+    PluginRegistry pluginRegistry = PluginRegistry.getInstance();
+    for (String className : guiRegistry.getShortCutsMap().keySet()) {
+      try {
+        IPlugin plugin =
+            pluginRegistry.getPlugins(GuiPluginType.class).stream()
+                .filter(p -> p.getClassMap().values().contains(className))
+                .findFirst()
+                .orElse(null);
+        if (plugin == null) {
+          continue;
+        }
+        findGuiPluginInstance(pluginRegistry.getClassLoader(plugin), 
className, instanceId);
+      } catch (Exception e) {
+        LogChannel.UI.logDebug(
+            "Could not pre-register shortcut plugin instance for "
+                + className
+                + ": "
+                + e.getMessage());
+      }
+    }
+  }
+
   public void createMenuWidgets(String root, Shell shell, Menu parent) {
     // Find the GUI Elements for the given class...
     //
@@ -183,7 +211,8 @@ public class GuiMenuWidgets extends BaseGuiWidgets {
                 guiMenuItem.getListenerClassName(), 
guiMenuItem.getListenerMethod(), Const.isOSX());
     if (shortcut != null) {
       appendShortCut(menuItem, shortcut);
-      menuItem.setAccelerator(getAccelerator(shortcut));
+      // Do not set menu accelerators; keyboard shortcuts are handled only by 
HopGuiKeyHandler
+      // so the correct context (focus) is used. Menu still shows shortcut 
text for discoverability.
       shortcutMap.put(guiMenuItem.getId(), shortcut);
     }
   }
@@ -211,74 +240,7 @@ public class GuiMenuWidgets extends BaseGuiWidgets {
   }
 
   public static String getShortcutString(KeyboardShortcut shortcut) {
-    String s = shortcut.toString();
-    if (StringUtils.isEmpty(s) || s.endsWith("+")) {
-      // Unknown characters from the SWT library
-      // We'll handle the special cases here.
-      //
-      int keyCode = shortcut.getKeyCode();
-      if (keyCode == SWT.BS) {
-        return s + "Backspace";
-      }
-      if (keyCode == SWT.ESC) {
-        return s + "Esc";
-      }
-      if (keyCode == SWT.DEL) {
-        return s + "Delete";
-      }
-      if (keyCode == SWT.ARROW_LEFT) {
-        return s + "Left";
-      }
-      if (keyCode == SWT.ARROW_RIGHT) {
-        return s + "Right";
-      }
-      if (keyCode == SWT.ARROW_UP) {
-        return s + "Up";
-      }
-      if (keyCode == SWT.ARROW_DOWN) {
-        return s + "Down";
-      }
-      if (keyCode == SWT.HOME) {
-        return s + "Home";
-      }
-      if (keyCode == SWT.F1) {
-        return s + "F1";
-      }
-      if (keyCode == SWT.F2) {
-        return s + "F2";
-      }
-      if (keyCode == SWT.F3) {
-        return s + "F3";
-      }
-      if (keyCode == SWT.F4) {
-        return s + "F4";
-      }
-      if (keyCode == SWT.F5) {
-        return s + "F5";
-      }
-      if (keyCode == SWT.F6) {
-        return s + "F6";
-      }
-      if (keyCode == SWT.F7) {
-        return s + "F7";
-      }
-      if (keyCode == SWT.F8) {
-        return s + "F8";
-      }
-      if (keyCode == SWT.F9) {
-        return s + "F9";
-      }
-      if (keyCode == SWT.F10) {
-        return s + "F10";
-      }
-      if (keyCode == SWT.F11) {
-        return s + "F11";
-      }
-      if (keyCode == SWT.F12) {
-        return s + "F12";
-      }
-    }
-    return s;
+    return ShortcutDisplayUtil.getShortcutDisplayString(shortcut);
   }
 
   /**
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
index cde9d9150d..a9f7306cb4 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
@@ -414,7 +414,10 @@ public class GuiToolbarWidgets extends BaseGuiWidgets 
implements IToolbarWidgetR
                 Const.isOSX());
     if (shortcut != null) {
       String tip = Const.NVL(guiToolbarItem.getToolTip(), "");
-      composite.setToolTipText(tip + " (" + shortcut + ')');
+      String shortcutText = 
ShortcutDisplayUtil.getShortcutDisplayString(shortcut);
+      if (!shortcutText.isEmpty()) {
+        composite.setToolTipText(tip + " (" + shortcutText + ')');
+      }
     }
   }
 
@@ -621,7 +624,10 @@ public class GuiToolbarWidgets extends BaseGuiWidgets 
implements IToolbarWidgetR
                 guiToolbarItem.getListenerMethod(),
                 Const.isOSX());
     if (shortcut != null) {
-      toolItem.setToolTipText(toolItem.getToolTipText() + " (" + shortcut + 
')');
+      String shortcutText = 
ShortcutDisplayUtil.getShortcutDisplayString(shortcut);
+      if (!shortcutText.isEmpty()) {
+        toolItem.setToolTipText(toolItem.getToolTipText() + " (" + 
shortcutText + ')');
+      }
     }
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/gui/ShortcutDisplayUtil.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/ShortcutDisplayUtil.java
new file mode 100644
index 0000000000..9fcc70bf0b
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/ShortcutDisplayUtil.java
@@ -0,0 +1,166 @@
+/*
+ * 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.gui;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.gui.plugin.key.KeyboardShortcut;
+import org.eclipse.swt.SWT;
+
+/**
+ * Shared utility for displaying keyboard shortcuts with OS-appropriate 
symbols (e.g. ⌘⇧⌥ on macOS).
+ * Used by menus, toolbars, and the keyboard shortcuts configuration tab so 
display is consistent.
+ */
+public final class ShortcutDisplayUtil {
+
+  private ShortcutDisplayUtil() {}
+
+  /**
+   * Returns the full shortcut string for menus and tooltips (e.g. "⌘⇧Z" on 
macOS, "Ctrl+Shift+Z" on
+   * Windows/Linux).
+   */
+  public static String getShortcutDisplayString(KeyboardShortcut shortcut) {
+    if (shortcut == null || shortcut.getKeyCode() == 0) {
+      return "";
+    }
+    List<String> parts = getModifierDisplayStrings(shortcut);
+    String keyText = getKeyDisplayText(shortcut.getKeyCode());
+    if (!keyText.isEmpty()) {
+      parts.add(keyText);
+    }
+    boolean isMacOS = Const.isOSX();
+    return isMacOS ? String.join("", parts) : String.join("+", parts);
+  }
+
+  /**
+   * Returns modifier symbols/labels in display order for menus and key 
badges. Uses the same
+   * canonical order everywhere so menu bar and config panel match: Control → 
Option → Shift →
+   * Command (⌃ ⌥ ⇧ ⌘ on macOS; Ctrl, Alt, Shift, Cmd on Windows/Linux). Does 
not include the main
+   * key.
+   */
+  public static List<String> getModifierDisplayStrings(KeyboardShortcut 
shortcut) {
+    List<String> out = new ArrayList<>();
+    boolean isMacOS = Const.isOSX();
+    // Same order on all platforms: Control, Option/Alt, Shift, Command
+    if (shortcut.isControl()) out.add(isMacOS ? "⌃" : "Ctrl");
+    if (shortcut.isAlt()) out.add(isMacOS ? "⌥" : "Alt");
+    if (shortcut.isShift()) out.add(isMacOS ? "⇧" : "Shift");
+    if (shortcut.isCommand()) out.add(isMacOS ? "⌘" : "Cmd");
+    return out;
+  }
+
+  /**
+   * Converts an SWT key code to the display string used in the configuration 
panel (e.g. "⌫" for
+   * Delete on macOS, "↑" for arrow up).
+   */
+  public static String getKeyDisplayText(int keyCode) {
+    boolean isMacOS = Const.isOSX();
+
+    if (keyCode == SWT.KEYPAD_ADD) return "+";
+    if (keyCode == SWT.KEYPAD_SUBTRACT) return "-";
+    if (keyCode == SWT.KEYPAD_MULTIPLY) return "*";
+    if (keyCode == SWT.KEYPAD_DIVIDE) return "/";
+    if (keyCode == SWT.KEYPAD_EQUAL) return "=";
+    if (keyCode == SWT.KEYPAD_DECIMAL) return ".";
+    if (keyCode == SWT.KEYPAD_CR) return "↵";
+    if (keyCode >= SWT.KEYPAD_0 && keyCode <= SWT.KEYPAD_9) {
+      return String.valueOf(keyCode - SWT.KEYPAD_0);
+    }
+
+    if (keyCode == 32) return "Space";
+    if (keyCode >= 65 && keyCode <= 90) return String.valueOf((char) keyCode);
+    if (keyCode >= 97 && keyCode <= 122)
+      return String.valueOf(Character.toUpperCase((char) keyCode));
+    if (keyCode == 96) return "`";
+    if (keyCode == 127) return isMacOS ? "⌫" : "Del";
+    if ((keyCode >= 48 && keyCode <= 57) || "+-/*=".indexOf(keyCode) >= 0) {
+      return String.valueOf((char) keyCode);
+    }
+
+    if ((keyCode & (1 << 24)) != 0) {
+      switch (keyCode & (0xFFFF)) {
+        case 1:
+          return "↑";
+        case 2:
+          return "↓";
+        case 3:
+          return "←";
+        case 4:
+          return "→";
+        case 5:
+          return "PgUp";
+        case 6:
+          return "PgDn";
+        case 7:
+          return "Home";
+        case 8:
+          return "End";
+        case 9:
+          return "Ins";
+        case 10:
+          return "F1";
+        case 11:
+          return "F2";
+        case 12:
+          return "F3";
+        case 13:
+          return "F4";
+        case 14:
+          return "F5";
+        case 15:
+          return "F6";
+        case 16:
+          return "F7";
+        case 17:
+          return "F8";
+        case 18:
+          return "F9";
+        case 19:
+          return "F10";
+        case 20:
+          return "F11";
+        case 21:
+          return "F12";
+        case 22:
+          return "F13";
+        case 23:
+          return "F14";
+        case 24:
+          return "F15";
+        case 25:
+          return "F16";
+        case 26:
+          return "F17";
+        case 27:
+          return "F18";
+        case 28:
+          return "F19";
+        case 29:
+          return "F20";
+        default:
+          break;
+      }
+    }
+
+    if (keyCode == SWT.ESC) return isMacOS ? "⎋" : "Esc";
+    if (keyCode == SWT.BS) return isMacOS ? "⌫" : "Backspace";
+
+    return "";
+  }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
index 3d941ed56e..1e8b13b1b8 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
@@ -64,6 +64,7 @@ import org.apache.hop.ui.core.gui.WindowProperty;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.core.widget.TreeUtil;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.HopGuiKeyHandler;
 import org.apache.hop.ui.hopgui.ToolbarFacade;
 import org.apache.hop.ui.hopgui.file.HopFileTypePluginType;
 import org.apache.hop.ui.hopgui.file.HopFileTypeRegistry;
@@ -106,7 +107,7 @@ import org.eclipse.swt.widgets.Tree;
 import org.eclipse.swt.widgets.TreeColumn;
 import org.eclipse.swt.widgets.TreeItem;
 
-@GuiPlugin(description = "Allows you to browse to local or VFS locations")
+@GuiPlugin(name = "File Browser", description = "Allows you to browse to local 
or VFS locations")
 public class HopVfsFileDialog implements IFileDialog, IDirectoryDialog {
 
   private static final Class<?> PKG = HopVfsFileDialog.class;
@@ -580,6 +581,11 @@ public class HopVfsFileDialog implements IFileDialog, 
IDirectoryDialog {
     //
     wFilename.setFocus();
 
+    // So Delete etc. work and we're tried before the active perspective
+    HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
+    keyHandler.addParentObjectToHandle(this, shell);
+    HopGui.getInstance().replaceKeyboardShortcutListeners(shell, keyHandler);
+
     shell.open();
 
     while (!shell.isDisposed()) {
@@ -1146,6 +1152,7 @@ public class HopVfsFileDialog implements IFileDialog, 
IDirectoryDialog {
     bookmarksToolbarWidgets.dispose();
     browserToolbarWidgets.dispose();
 
+    HopGuiKeyHandler.getInstance().removeParentObjectToHandle(this);
     shell.dispose();
   }
 
@@ -1401,7 +1408,6 @@ public class HopVfsFileDialog implements IFileDialog, 
IDirectoryDialog {
       id = BROWSER_ITEM_ID_DELETE,
       toolTip = "i18n::HopVfsFileDialog.DeleteFile.Tooltip.Message",
       image = "ui/images/delete.svg")
-  // FIXME: Keyboard don't work
   @GuiKeyboardShortcut(key = SWT.DEL)
   @GuiOsxKeyboardShortcut(key = SWT.DEL)
   public void deleteFile() {
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 8ff9a4e887..e87f98b96f 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
@@ -160,7 +160,7 @@ import org.eclipse.swt.widgets.ToolItem;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
 
-@GuiPlugin(description = "The main hop graphical user interface")
+@GuiPlugin(name = "Hop Gui", description = "The main hop graphical user 
interface")
 @SuppressWarnings("java:S1104")
 @Getter
 @Setter
@@ -954,6 +954,7 @@ public class HopGui
 
     mainMenu = new Menu(shell, SWT.BAR);
     mainMenuWidgets.createMenuWidgets(ID_MAIN_MENU, shell, mainMenu);
+    mainMenuWidgets.ensureShortcutPluginInstancesRegistered();
 
     if (EnvironmentUtils.getInstance().isWeb()) {
       mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_FILE_EXIT, false);
@@ -1413,38 +1414,6 @@ public class HopGui
     // Nothing is done here.
   }
 
-  @GuiMenuElement(
-      root = ID_MAIN_MENU,
-      id = ID_MAIN_MENU_VIEW_TERMINAL,
-      label = "i18n::HopGui.Menu.View.Terminal",
-      parentId = ID_MAIN_MENU_VIEW_PARENT_ID)
-  @GuiKeyboardShortcut(control = true, key = '`')
-  @GuiOsxKeyboardShortcut(control = true, key = '`')
-  public void menuViewTerminal() {
-    if (EnvironmentUtils.getInstance().isWeb()) {
-      return;
-    }
-    if (terminalPanel != null) {
-      terminalPanel.toggleTerminal();
-    }
-  }
-
-  @GuiMenuElement(
-      root = ID_MAIN_MENU,
-      id = ID_MAIN_MENU_VIEW_NEW_TERMINAL,
-      label = "i18n::HopGui.Menu.View.NewTerminal",
-      parentId = ID_MAIN_MENU_VIEW_PARENT_ID)
-  @GuiKeyboardShortcut(control = true, shift = true, key = '`')
-  @GuiOsxKeyboardShortcut(control = true, shift = true, key = '`')
-  public void menuViewNewTerminal() {
-    if (EnvironmentUtils.getInstance().isWeb()) {
-      return;
-    }
-    if (terminalPanel != null) {
-      terminalPanel.createNewTerminal(null, null);
-    }
-  }
-
   // ======================== Run Menu ========================
 
   @GuiMenuElement(
@@ -1705,6 +1674,22 @@ public class HopGui
       fdTerminalPanel.right = new FormAttachment(100, 0);
       terminalPanel.setLayoutData(fdTerminalPanel);
 
+      // Register so Tools > Terminal menu items can invoke panel methods
+      String menuInstanceId = mainMenuWidgets.getInstanceId();
+      org.apache.hop.core.gui.plugin.GuiRegistry.getInstance()
+          .registerGuiPluginObject(
+              getId(),
+              
org.apache.hop.ui.hopgui.terminal.HopGuiTerminalPanel.class.getName(),
+              menuInstanceId,
+              terminalPanel);
+      terminalPanel.addDisposeListener(
+          e ->
+              org.apache.hop.core.gui.plugin.GuiRegistry.getInstance()
+                  .removeGuiPluginObject(
+                      getId(),
+                      
org.apache.hop.ui.hopgui.terminal.HopGuiTerminalPanel.class.getName(),
+                      menuInstanceId));
+
       mainPerspectivesComposite = terminalPanel.getPerspectiveComposite();
       mainPerspectivesComposite.setLayout(new StackLayout());
     }
@@ -1842,11 +1827,7 @@ public class HopGui
     return getActivePerspective().getActiveFileTypeHandler();
   }
 
-  /**
-   * Replace the listeners based on the @{@link GuiKeyboardShortcut} 
annotations
-   *
-   * @param parentObject The parent object containing the annotations and 
methods
-   */
+  /** Register parent and attach key handler to shell + children. */
   public void replaceKeyboardShortcutListeners(Object parentObject) {
     HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
     keyHandler.addParentObjectToHandle(parentObject);
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java
index e3bfa0fac8..c4580c8447 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java
@@ -18,8 +18,12 @@
 package org.apache.hop.ui.hopgui;
 
 import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.gui.plugin.GuiRegistry;
@@ -41,6 +45,9 @@ public class HopGuiKeyHandler extends KeyAdapter {
 
   public Set<Object> parentObjects;
 
+  /** Parent -> Control (e.g. shell) so we try that parent when focus is in 
its window. */
+  private final Map<Object, Control> parentToControl = new HashMap<>();
+
   private KeyboardShortcut lastShortcut;
   private long lastShortcutTime;
 
@@ -59,16 +66,21 @@ public class HopGuiKeyHandler extends KeyAdapter {
     parentObjects.add(parentObject);
   }
 
+  /** Register parent with its window control so shortcuts in that window take 
precedence. */
+  public void addParentObjectToHandle(Object parentObject, Control control) {
+    parentObjects.add(parentObject);
+    if (control != null) {
+      parentToControl.put(parentObject, control);
+    }
+  }
+
   public void removeParentObjectToHandle(Object parentObject) {
     parentObjects.remove(parentObject);
+    parentToControl.remove(parentObject);
   }
 
   @Override
   public void keyPressed(KeyEvent event) {
-    // TODO: allow for keyboard shortcut priorities for certain objects.
-    //
-
-    // If the event has already been handled by another listener (e.g. 
terminal), skip it
     if (!event.doit) {
       return;
     }
@@ -91,53 +103,110 @@ public class HopGuiKeyHandler extends KeyAdapter {
       }
     }
 
-    for (Object parentObject : parentObjects) {
+    List<Object> orderedParents = getParentObjectsInContextOrder(event.widget);
+    for (Object parentObject : orderedParents) {
       List<KeyboardShortcut> shortcuts =
           
GuiRegistry.getInstance().getKeyboardShortcuts(parentObject.getClass().getName());
       if (shortcuts != null) {
         for (KeyboardShortcut shortcut : shortcuts) {
           if (handleKey(parentObject, event, shortcut)) {
             event.doit = false;
-            return; // This key is handled.
+            return;
           }
         }
       }
     }
   }
 
-  private boolean handleKey(Object parentObject, KeyEvent event, 
KeyboardShortcut shortcut) {
-    // If this is a control, only handle the shortcut if the control is 
visible.
-    // This prevents keyboard shortcuts being applied to a workflow or 
pipeline which
-    // isn't visible (in another tab, for example).
-    //
+  /** Order: parents whose window has focus (closest first), then active 
perspectives, then rest. */
+  private List<Object> getParentObjectsInContextOrder(Object focusedWidget) {
+    List<Object> inFocus = new ArrayList<>();
+    List<Object> fallback = new ArrayList<>();
+    for (Object parent : parentObjects) {
+      Control control = parent instanceof Control c ? c : 
parentToControl.get(parent);
+      if (control != null && isWidgetInControlHierarchy(focusedWidget, 
control)) {
+        inFocus.add(parent);
+      } else {
+        fallback.add(parent);
+      }
+    }
+    inFocus.sort(
+        Comparator.comparingInt(
+            p -> {
+              Control c = p instanceof Control x ? x : parentToControl.get(p);
+              return c != null ? getDepthFromWidgetToControl(focusedWidget, c) 
: Integer.MAX_VALUE;
+            }));
+    fallback.sort(
+        (a, b) -> {
+          boolean aActive = isActivePerspective(a);
+          boolean bActive = isActivePerspective(b);
+          if (aActive && !bActive) return -1;
+          if (!aActive && bActive) return 1;
+          return 0;
+        });
+    List<Object> result = new ArrayList<>(inFocus);
+    result.addAll(fallback);
+    return result;
+  }
+
+  private boolean isActivePerspective(Object parent) {
+    if (parent instanceof IHopPerspective perspective) {
+      try {
+        return perspective.isActive();
+      } catch (Exception e) {
+        return false;
+      }
+    }
+    return false;
+  }
+
+  /** Depth from widget to control (1 = direct parent). */
+  private int getDepthFromWidgetToControl(Object widget, Control control) {
+    if (!(widget instanceof Control)) {
+      return Integer.MAX_VALUE;
+    }
+    int depth = 0;
+    Control current = (Control) widget;
+    while (current != null) {
+      if (current == control) {
+        return depth;
+      }
+      depth++;
+      try {
+        current = current.getParent();
+      } catch (Exception e) {
+        return Integer.MAX_VALUE;
+      }
+    }
+    return Integer.MAX_VALUE;
+  }
+
+  private boolean isParentInContext(
+      Object parentObject, KeyEvent event, KeyboardShortcut shortcut) {
     if (parentObject instanceof Control control) {
       try {
         if (!control.isVisible()) {
-          return false;
-        }
-        // Also skip if the event widget (focused widget) is NOT within this 
control's hierarchy
-        // This prevents pipeline/workflow shortcuts from firing when focus is 
in the terminal
-        if (!isWidgetInControlHierarchy(event.widget, control)) {
-          return false;
+          return shortcut.isGlobal();
         }
+        return shortcut.isGlobal() || isWidgetInControlHierarchy(event.widget, 
control);
       } catch (SWTException e) {
-        // Invalid thread: none of our business, bail out
-        //
         return false;
       }
     }
-    // If this is attached to a perspective, and it's not active, bail out.
-    // Except if it's shortcut to activate perspective
     if (parentObject instanceof IHopPerspective perspective) {
       try {
-        // TODO: It's not the best way to check with the method name, but it 
works for now.
-        if (!perspective.isActive() && 
!shortcut.getParentMethodName().equals("activate")) {
-          return false;
-        }
-      } catch (Exception ex) {
+        return perspective.isActive() || shortcut.isGlobal();
+      } catch (Exception e) {
         return false;
       }
     }
+    return true;
+  }
+
+  private boolean handleKey(Object parentObject, KeyEvent event, 
KeyboardShortcut shortcut) {
+    if (!isParentInContext(parentObject, event, shortcut)) {
+      return false;
+    }
 
     int keyCode = (event.keyCode & SWT.KEY_MASK);
 
@@ -153,16 +222,25 @@ public class HopGuiKeyHandler extends KeyAdapter {
     else if (keyCode == SWT.KEYPAD_MULTIPLY) keyCode = '*';
     else if (keyCode == SWT.KEYPAD_DIVIDE) keyCode = '/';
     else if (keyCode == SWT.KEYPAD_EQUAL) keyCode = '=';
+    // Backtick: in SWT use event.character ('`' = 96); keyCode may be 0 or 
192 (VK_OEM_3) on some
+    // platforms
+    else if (keyCode == 192) keyCode = '`';
 
-    boolean keyMatch = keyCode == shortcut.getKeyCode();
+    int shortcutKey = shortcut.getKeyCode();
+    // Match by keyCode, or by event.character (SWT maps backtick/grave accent 
to event.character)
+    boolean keyMatch =
+        keyCode == shortcutKey || (shortcutKey != 0 && event.character == 
shortcutKey);
     boolean altMatch = shortcut.isAlt() == alt;
     boolean shiftMatch = shortcut.isShift() == shift;
     boolean controlMatch = shortcut.isControl() == control;
     boolean commandMatch = shortcut.isCommand() == command;
 
     if (matchOS && keyMatch && altMatch && shiftMatch && controlMatch && 
commandMatch) {
-      // This is the key: call the method to which the original key shortcut 
annotation belongs
-      //
+      // Only invoke if this shortcut is linked to this class (context)
+      if (shortcut.getParentClassName() != null
+          && 
!shortcut.getParentClassName().equals(parentObject.getClass().getName())) {
+        return false;
+      }
       try {
         Class<?> parentClass = parentObject.getClass();
         Method method = parentClass.getMethod(shortcut.getParentMethodName());
@@ -179,13 +257,6 @@ public class HopGuiKeyHandler extends KeyAdapter {
     return false;
   }
 
-  /**
-   * Check if a widget is within the hierarchy of a control
-   *
-   * @param widget The widget to check (typically the focused widget)
-   * @param control The parent control to check against
-   * @return true if widget is within control's hierarchy, false otherwise
-   */
   private boolean isWidgetInControlHierarchy(Object widget, Control control) {
     if (!(widget instanceof Control)) {
       return false;
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index 76714bd3e0..a72b4ef0c5 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -179,7 +179,6 @@ import 
org.apache.hop.ui.hopgui.file.shared.PipelineRowSamplerHelper;
 import org.apache.hop.ui.hopgui.perspective.execution.ExecutionPerspective;
 import org.apache.hop.ui.hopgui.perspective.execution.IExecutionViewer;
 import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
-import org.apache.hop.ui.hopgui.selection.HopGuiSelectionTracker;
 import org.apache.hop.ui.hopgui.shared.CanvasZoomHelper;
 import org.apache.hop.ui.hopgui.shared.SwtGc;
 import org.apache.hop.ui.pipeline.dialog.PipelineDialog;
@@ -771,9 +770,7 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
             selectedTransforms = pipelineMeta.getSelectedTransforms();
             selectedTransform = currentTransform;
 
-            // Track that a transform was selected
-            HopGuiSelectionTracker.getInstance()
-                
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+            pipelineGridDelegate.onPipelineSelectionChanged();
 
             for (ITransformSelectionListener listener : 
currentTransformListeners) {
               listener.onUpdateSelection(currentTransform);
@@ -843,8 +840,7 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
       selectedNotes = pipelineMeta.getSelectedNotes();
       selectedNote = currentNotePad;
       // Track that a note was selected
-      HopGuiSelectionTracker.getInstance()
-          
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+      pipelineGridDelegate.onPipelineSelectionChanged();
       Point loc = currentNotePad.getLocation();
 
       previousNoteLocations = pipelineMeta.getSelectedNoteLocations();
@@ -1022,8 +1018,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
         // Track that transforms were selected via region selection
         if (!pipelineMeta.getSelectedTransforms().isEmpty()
             || !pipelineMeta.getSelectedNotes().isEmpty()) {
-          HopGuiSelectionTracker.getInstance()
-              
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+          pipelineGridDelegate.onPipelineSelectionChanged();
         }
         updateGui();
         return;
@@ -1079,8 +1074,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
             selectedTransform.flipSelected();
             // Track that a transform selection changed (if it's now selected)
             if (selectedTransform.isSelected()) {
-              HopGuiSelectionTracker.getInstance()
-                  
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+              pipelineGridDelegate.onPipelineSelectionChanged();
             }
           } else {
             singleClick = true;
@@ -1094,8 +1088,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
               selectedTransform.setSelected(true);
             }
             // Track that a transform was selected
-            HopGuiSelectionTracker.getInstance()
-                
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+            pipelineGridDelegate.onPipelineSelectionChanged();
           }
         } else {
           // Find out which Transforms & Notes are selected
@@ -1103,8 +1096,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
           selectedNotes = pipelineMeta.getSelectedNotes();
           // Track that transforms were selected
           if (!selectedTransforms.isEmpty() || !selectedNotes.isEmpty()) {
-            HopGuiSelectionTracker.getInstance()
-                
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+            pipelineGridDelegate.onPipelineSelectionChanged();
           }
 
           // We moved around some items: store undo info...
@@ -1167,8 +1159,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
               selectedNote.flipSelected();
               // Track that a note selection changed (if it's now selected)
               if (selectedNote.isSelected()) {
-                HopGuiSelectionTracker.getInstance()
-                    
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+                pipelineGridDelegate.onPipelineSelectionChanged();
               }
             } else {
               // single click on a note: ask what needs to happen...
@@ -1184,8 +1175,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
                 selectedNote.setSelected(true);
               }
               // Track that a note was selected
-              HopGuiSelectionTracker.getInstance()
-                  
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+              pipelineGridDelegate.onPipelineSelectionChanged();
             }
           } else {
             // Find out which Transforms & Notes are selected
@@ -1193,8 +1183,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
             selectedNotes = pipelineMeta.getSelectedNotes();
             // Track that notes were selected
             if (!selectedTransforms.isEmpty() || !selectedNotes.isEmpty()) {
-              HopGuiSelectionTracker.getInstance()
-                  
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+              pipelineGridDelegate.onPipelineSelectionChanged();
             }
 
             // We moved around some items: store undo info...
@@ -1620,8 +1609,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       selectedTransforms.add(selectedTransform);
       previousTransformLocations = new Point[] 
{selectedTransform.getLocation()};
       // Track that a transform was selected
-      HopGuiSelectionTracker.getInstance()
-          
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+      pipelineGridDelegate.onPipelineSelectionChanged();
       doRedraw = true;
     } else if (selectedNote != null && !selectedNote.isSelected()) {
       pipelineMeta.unselectAll();
@@ -1630,8 +1618,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       selectedNotes.add(selectedNote);
       previousNoteLocations = new Point[] {selectedNote.getLocation()};
       // Track that a note was selected
-      HopGuiSelectionTracker.getInstance()
-          
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
+      pipelineGridDelegate.onPipelineSelectionChanged();
       doRedraw = true;
     } else if (selectionRegion != null && startHopTransform == null) {
       // Did we select a region...?
@@ -5440,19 +5427,13 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   @GuiOsxKeyboardShortcut(key = SWT.DEL)
   @Override
   public void deleteSelected() {
-    // Only handle delete if a pipeline graph item was the last selected item
-    // OR if there are actually selected transforms/notes in the pipeline
-    HopGuiSelectionTracker selectionTracker = 
HopGuiSelectionTracker.getInstance();
+    // Shortcut only fires when focus is in this graph; delete if we have 
selection
     boolean hasPipelineSelection =
         !pipelineMeta.getSelectedTransforms().isEmpty()
             || !pipelineMeta.getSelectedNotes().isEmpty();
-    boolean isLastPipelineSelection =
-        
selectionTracker.isLastSelection(HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH);
-
-    if (!isLastPipelineSelection || !hasPipelineSelection) {
+    if (!hasPipelineSelection) {
       return;
     }
-
     delSelected(null);
     updateGui();
   }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
index 988f92d402..1b19de3a9c 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
@@ -62,7 +62,6 @@ import org.apache.hop.ui.hopgui.ToolbarFacade;
 import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
 import org.apache.hop.ui.hopgui.file.pipeline.PipelineMetricDisplayUtil;
-import org.apache.hop.ui.hopgui.selection.HopGuiSelectionTracker;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CTabItem;
 import org.eclipse.swt.graphics.Color;
@@ -334,18 +333,6 @@ public class HopGuiPipelineGridDelegate {
 
     startRefreshMetricsTimer();
     pipelineGridTab.addDisposeListener(disposeEvent -> 
stopRefreshMetricsTimer());
-    HopGuiSelectionTracker.getInstance()
-        .addSelectionListener(
-            HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH,
-            () -> {
-              if (!showSelectedTransforms) {
-                return;
-              }
-              if (pipelineGridView == null || pipelineGridView.isDisposed()) {
-                return;
-              }
-              hopGui.getDisplay().asyncExec(this::refreshView);
-            });
 
     pipelineGridTab.setControl(pipelineGridComposite);
   }
@@ -385,6 +372,20 @@ public class HopGuiPipelineGridDelegate {
     refreshMetricsTimer = null;
   }
 
+  /**
+   * Called by the pipeline graph when selection changes (e.g. user selects 
transforms). Refreshes
+   * the grid view when "show selected" is on.
+   */
+  public void onPipelineSelectionChanged() {
+    if (!showSelectedTransforms) {
+      return;
+    }
+    if (pipelineGridView == null || pipelineGridView.isDisposed()) {
+      return;
+    }
+    hopGui.getDisplay().asyncExec(this::refreshView);
+  }
+
   /**
    * When a toolbar is hit it knows the class so it will come here to ask for 
the instance.
    *
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index 6bdf21a40b..c322f3f4d6 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -138,7 +138,6 @@ import 
org.apache.hop.ui.hopgui.file.workflow.extension.HopGuiWorkflowGraphExten
 import org.apache.hop.ui.hopgui.perspective.execution.ExecutionPerspective;
 import org.apache.hop.ui.hopgui.perspective.execution.IExecutionViewer;
 import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
-import org.apache.hop.ui.hopgui.selection.HopGuiSelectionTracker;
 import org.apache.hop.ui.hopgui.shared.CanvasZoomHelper;
 import org.apache.hop.ui.hopgui.shared.SwtGc;
 import org.apache.hop.ui.util.EnvironmentUtils;
@@ -721,8 +720,6 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
       selectedNotes = workflowMeta.getSelectedNotes();
       selectedNote = currentNotePad;
       // Track that a note was selected
-      HopGuiSelectionTracker.getInstance()
-          
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.WORKFLOW_GRAPH);
       Point loc = currentNotePad.getLocation();
 
       previousNoteLocations = workflowMeta.getSelectedNoteLocations();
@@ -897,12 +894,6 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
         workflowMeta.unselectAll();
         selectInRect(workflowMeta, selectionRegion);
         selectionRegion = null;
-        // Track that actions/notes were selected via region selection
-        if (!workflowMeta.getSelectedActions().isEmpty()
-            || !workflowMeta.getSelectedNotes().isEmpty()) {
-          HopGuiSelectionTracker.getInstance()
-              
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.WORKFLOW_GRAPH);
-        }
         updateGui();
         return;
       }
@@ -956,19 +947,11 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
               selectedAction.setSelected(true);
             }
             // Track that an action was selected
-            HopGuiSelectionTracker.getInstance()
-                
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.WORKFLOW_GRAPH);
           }
         } else {
           // Find out which Transforms & Notes are selected
           selectedActions = workflowMeta.getSelectedActions();
           selectedNotes = workflowMeta.getSelectedNotes();
-          // Track that actions/notes were selected
-          if (!selectedActions.isEmpty() || !selectedNotes.isEmpty()) {
-            HopGuiSelectionTracker.getInstance()
-                
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.WORKFLOW_GRAPH);
-          }
-
           // We moved around some items: store undo info...
           //
           boolean also = false;
@@ -1061,18 +1044,13 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
                 selectedNote.setSelected(true);
               }
               // Track that a note was selected
-              HopGuiSelectionTracker.getInstance()
-                  
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.WORKFLOW_GRAPH);
             }
           } else {
             // Find out which Transforms & Notes are selected
             selectedActions = workflowMeta.getSelectedActions();
             selectedNotes = workflowMeta.getSelectedNotes();
             // Track that actions/notes were selected
-            if (!selectedActions.isEmpty() || !selectedNotes.isEmpty()) {
-              HopGuiSelectionTracker.getInstance()
-                  
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.WORKFLOW_GRAPH);
-            }
+            if (!selectedActions.isEmpty() || !selectedNotes.isEmpty()) {}
 
             // We moved around some items: store undo info...
             boolean also = false;
@@ -2170,12 +2148,7 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
   @GuiOsxKeyboardShortcut(key = SWT.DEL)
   @Override
   public void deleteSelected() {
-    // Only handle delete if a workflow graph item was the last selected item
-    HopGuiSelectionTracker selectionTracker = 
HopGuiSelectionTracker.getInstance();
-    if 
(!selectionTracker.isLastSelection(HopGuiSelectionTracker.SelectionType.WORKFLOW_GRAPH))
 {
-      return;
-    }
-
+    // Shortcut only fires when focus is in this graph
     deleteSelected(null);
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
index 8e9167a1cc..8bf953eb6f 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
@@ -38,6 +38,7 @@ import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.HopGuiKeyHandler;
 import org.apache.hop.ui.hopgui.context.IGuiContextHandler;
 import org.apache.hop.ui.hopgui.perspective.HopPerspectivePlugin;
 import org.apache.hop.ui.hopgui.perspective.IHopPerspective;
@@ -58,6 +59,7 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.Table;
 import org.eclipse.swt.widgets.TableItem;
 import org.eclipse.swt.widgets.Text;
@@ -107,8 +109,8 @@ public class ConfigurationPerspective implements 
IHopPerspective {
     return "configuration";
   }
 
-  @GuiKeyboardShortcut(control = true, shift = true, key = 'c')
-  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'c')
+  @GuiKeyboardShortcut(control = true, shift = true, key = 'c', global = true)
+  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'c', global = 
true)
   @Override
   public void activate() {
     hopGui.setActivePerspective(this);
@@ -244,6 +246,14 @@ public class ConfigurationPerspective implements 
IHopPerspective {
     loadSettingCategories();
 
     sashForm.setWeights(20, 80);
+
+    // Register with key handler so activate shortcut (Ctrl+Shift+C / 
Cmd+Shift+C) works
+    HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
+    keyHandler.addParentObjectToHandle(this);
+    Shell shell = (sashForm != null && !sashForm.isDisposed()) ? 
sashForm.getShell() : null;
+    if (shell != null) {
+      HopGui.getInstance().replaceKeyboardShortcutListeners(shell, keyHandler);
+    }
   }
 
   private void loadSettingCategories() {
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigKeyboardShortcutsTab.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigKeyboardShortcutsTab.java
index 38dae0cd99..3bd013a493 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigKeyboardShortcutsTab.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigKeyboardShortcutsTab.java
@@ -24,15 +24,19 @@ import java.util.List;
 import java.util.Map;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiPluginType;
 import org.apache.hop.core.gui.plugin.GuiRegistry;
 import org.apache.hop.core.gui.plugin.key.KeyboardShortcut;
 import org.apache.hop.core.gui.plugin.tab.GuiTab;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
 import org.apache.hop.core.util.TranslateUtil;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.dialog.BaseDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.ShortcutDisplayUtil;
 import 
org.apache.hop.ui.hopgui.perspective.configuration.ConfigurationPerspective;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CTabFolder;
@@ -101,7 +105,7 @@ public class ConfigKeyboardShortcutsTab {
       monoFont = new Font(display, monoFontData);
     }
 
-    // Get all keyboard shortcuts from the registry
+    // Get all keyboard shortcuts from the registry (sorted by method name, 
then OS)
     GuiRegistry guiRegistry = GuiRegistry.getInstance();
     Map<String, List<KeyboardShortcut>> shortcutsMap = 
guiRegistry.getShortCutsMap();
 
@@ -114,7 +118,7 @@ public class ConfigKeyboardShortcutsTab {
     Collections.sort(classNames);
 
     for (String className : classNames) {
-      List<KeyboardShortcut> allShortcuts = shortcutsMap.get(className);
+      List<KeyboardShortcut> allShortcuts = 
guiRegistry.getKeyboardShortcuts(className);
       if (allShortcuts == null || allShortcuts.isEmpty()) {
         continue;
       }
@@ -282,44 +286,12 @@ public class ConfigKeyboardShortcutsTab {
     fdKeys.width = 200; // Fixed width for shortcut column
     keysComposite.setLayoutData(fdKeys);
 
-    // Add modifier keys and main key
-    // Standard order for macOS: Command, Shift, Alt, Control (Apple 
convention)
-    // Standard order for other OS: Control, Alt, Shift
-    boolean isMacOS = Const.isOSX();
+    // Add modifier keys and main key (same order and symbols as 
menus/toolbars)
     Control lastKey = null;
-
-    if (isMacOS) {
-      // macOS order: Command first, then Shift, then Alt/Option, then Control
-      if (shortcut.isCommand()) {
-        lastKey = createKeyBadge(keysComposite, "⌘", lastKey, margin);
-      }
-      if (shortcut.isShift()) {
-        lastKey = createKeyBadge(keysComposite, "⇧", lastKey, margin);
-      }
-      if (shortcut.isAlt()) {
-        lastKey = createKeyBadge(keysComposite, "⌥", lastKey, margin);
-      }
-      if (shortcut.isControl()) {
-        lastKey = createKeyBadge(keysComposite, "⌃", lastKey, margin);
-      }
-    } else {
-      // Windows/Linux order: Control, Alt, Shift, Command
-      if (shortcut.isControl()) {
-        lastKey = createKeyBadge(keysComposite, "Ctrl", lastKey, margin);
-      }
-      if (shortcut.isAlt()) {
-        lastKey = createKeyBadge(keysComposite, "Alt", lastKey, margin);
-      }
-      if (shortcut.isShift()) {
-        lastKey = createKeyBadge(keysComposite, "Shift", lastKey, margin);
-      }
-      if (shortcut.isCommand()) {
-        lastKey = createKeyBadge(keysComposite, "Cmd", lastKey, margin);
-      }
+    for (String modifier : 
ShortcutDisplayUtil.getModifierDisplayStrings(shortcut)) {
+      lastKey = createKeyBadge(keysComposite, modifier, lastKey, margin);
     }
-
-    // Add the main key
-    String keyText = getKeyText(shortcut.getKeyCode());
+    String keyText = 
ShortcutDisplayUtil.getKeyDisplayText(shortcut.getKeyCode());
     if (!Utils.isEmpty(keyText)) {
       lastKey = createKeyBadge(keysComposite, keyText, lastKey, margin);
     }
@@ -409,130 +381,6 @@ public class ConfigKeyboardShortcutsTab {
     return 32;
   }
 
-  /**
-   * Converts a key code to a readable string.
-   *
-   * @param keyCode The SWT key code
-   * @return A readable key string
-   */
-  private String getKeyText(int keyCode) {
-    boolean isMacOS = Const.isOSX();
-
-    // Keypad key conversions (match the logic in HopGuiKeyHandler)
-    if (keyCode == SWT.KEYPAD_ADD) {
-      return "+";
-    } else if (keyCode == SWT.KEYPAD_SUBTRACT) {
-      return "-";
-    } else if (keyCode == SWT.KEYPAD_MULTIPLY) {
-      return "*";
-    } else if (keyCode == SWT.KEYPAD_DIVIDE) {
-      return "/";
-    } else if (keyCode == SWT.KEYPAD_EQUAL) {
-      return "=";
-    } else if (keyCode == SWT.KEYPAD_DECIMAL) {
-      return ".";
-    } else if (keyCode == SWT.KEYPAD_CR) {
-      return "↵";
-    } else if (keyCode >= SWT.KEYPAD_0 && keyCode <= SWT.KEYPAD_9) {
-      // Keypad 0-9
-      return String.valueOf(keyCode - SWT.KEYPAD_0);
-    }
-
-    // Spacebar
-    if (keyCode == 32) {
-      return "Space";
-    }
-    // Character upper
-    else if (keyCode >= 65 && keyCode <= 90) {
-      return String.valueOf((char) keyCode);
-    }
-    // Character lower
-    else if (keyCode >= 97 && keyCode <= 122) {
-      return String.valueOf(Character.toUpperCase((char) keyCode));
-    }
-    // Delete key
-    else if (keyCode == 127) {
-      return isMacOS ? "⌫" : "Del";
-    }
-    // Digit and common symbols
-    else if ((keyCode >= 48 && keyCode <= 57) || "+-/*=".indexOf(keyCode) >= 
0) {
-      return String.valueOf((char) keyCode);
-    }
-
-    // Special keys (SWT constants)
-    if ((keyCode & (1 << 24)) != 0) {
-      switch (keyCode & (0xFFFF)) {
-        case 1:
-          return "↑";
-        case 2:
-          return "↓";
-        case 3:
-          return "←";
-        case 4:
-          return "→";
-        case 5:
-          return "PgUp";
-        case 6:
-          return "PgDn";
-        case 7:
-          return "Home";
-        case 8:
-          return "End";
-        case 9:
-          return "Ins";
-        case 10:
-          return "F1";
-        case 11:
-          return "F2";
-        case 12:
-          return "F3";
-        case 13:
-          return "F4";
-        case 14:
-          return "F5";
-        case 15:
-          return "F6";
-        case 16:
-          return "F7";
-        case 17:
-          return "F8";
-        case 18:
-          return "F9";
-        case 19:
-          return "F10";
-        case 20:
-          return "F11";
-        case 21:
-          return "F12";
-        case 22:
-          return "F13";
-        case 23:
-          return "F14";
-        case 24:
-          return "F15";
-        case 25:
-          return "F16";
-        case 26:
-          return "F17";
-        case 27:
-          return "F18";
-        case 28:
-          return "F19";
-        case 29:
-          return "F20";
-        default:
-          break;
-      }
-    }
-
-    // ESC key
-    if (keyCode == SWT.ESC) {
-      return isMacOS ? "⎋" : "Esc";
-    }
-
-    return "";
-  }
-
   /**
    * Formats a method name to be more readable.
    *
@@ -560,25 +408,31 @@ public class ConfigKeyboardShortcutsTab {
     return formatted.toString();
   }
 
-  /**
-   * Gets a friendly plugin name from a fully qualified class name. Checks 
GuiPlugin annotation for
-   * name or id, otherwise falls back to the simple class name.
-   *
-   * @param className The fully qualified class name
-   * @return The plugin name, id, or simple class name
-   */
+  /** Plugin name from class name: registry first, then @GuiPlugin, then 
simple name. */
   private String getPluginName(String className) {
     if (Utils.isEmpty(className)) {
       return "";
     }
 
     try {
-      // Try to load the class and get its GuiPlugin annotation
+      String pluginName =
+          PluginRegistry.getInstance().getPlugins(GuiPluginType.class).stream()
+              .filter(p -> p.getClassMap().values().contains(className))
+              .findFirst()
+              .map(IPlugin::getName)
+              .orElse(null);
+      if (!Utils.isEmpty(pluginName)) {
+        return pluginName;
+      }
+    } catch (Exception e) {
+      // Fall through
+    }
+
+    try {
       Class<?> clazz = Class.forName(className);
       GuiPlugin annotation = clazz.getAnnotation(GuiPlugin.class);
 
       if (annotation != null) {
-        // First try to get the name
         if (!Utils.isEmpty(annotation.name())) {
           String name = TranslateUtil.translate(annotation.name(), clazz);
           if (!Utils.isEmpty(name)) {
@@ -586,16 +440,14 @@ public class ConfigKeyboardShortcutsTab {
           }
         }
 
-        // Fall back to id if name is empty
         if (!Utils.isEmpty(annotation.id())) {
           return annotation.id();
         }
       }
     } catch (Exception e) {
-      // Fall through to simple class name
+      // ignore
     }
 
-    // Fallback to simple class name
     int lastDot = className.lastIndexOf('.');
     if (lastDot >= 0 && lastDot < className.length() - 1) {
       return className.substring(lastDot + 1);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
index 071de715b7..0993e5f93b 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
@@ -141,8 +141,8 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
     return "execution-perspective";
   }
 
-  @GuiKeyboardShortcut(control = true, shift = true, key = 'i')
-  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'i')
+  @GuiKeyboardShortcut(control = true, shift = true, key = 'i', global = true)
+  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'i', global = 
true)
   @Override
   public void activate() {
     hopGui.setActivePerspective(this);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index a8109e4ff5..585b9ff258 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -99,7 +99,6 @@ import 
org.apache.hop.ui.hopgui.perspective.explorer.file.ExplorerFileType;
 import 
org.apache.hop.ui.hopgui.perspective.explorer.file.IExplorerFileTypeHandler;
 import org.apache.hop.ui.hopgui.perspective.explorer.file.types.FolderFileType;
 import 
org.apache.hop.ui.hopgui.perspective.explorer.file.types.GenericFileType;
-import org.apache.hop.ui.hopgui.selection.HopGuiSelectionTracker;
 import org.apache.hop.ui.hopgui.shared.CanvasZoomHelper;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.engine.IWorkflowEngine;
@@ -248,8 +247,8 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
     return "explorer-perspective";
   }
 
-  @GuiKeyboardShortcut(control = true, shift = true, key = 'd')
-  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'd')
+  @GuiKeyboardShortcut(control = true, shift = true, key = 'd', global = true)
+  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'd', global = 
true)
   @Override
   public void activate() {
     hopGui.setActivePerspective(this);
@@ -1746,12 +1745,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
   @GuiKeyboardShortcut(key = SWT.DEL)
   @GuiOsxKeyboardShortcut(key = SWT.DEL)
   public void deleteFile() {
-    // Only handle delete if a file was the last selected item
-    HopGuiSelectionTracker selectionTracker = 
HopGuiSelectionTracker.getInstance();
-    if 
(!selectionTracker.isLastSelection(HopGuiSelectionTracker.SelectionType.FILE_EXPLORER))
 {
-      return;
-    }
-
+    // Shortcut only fires when focus is in file explorer
     TreeItem[] selection = tree.getSelection();
     if (selection == null || selection.length == 0) {
       return;
@@ -2375,9 +2369,6 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
       if (tif == null) {
         return;
       }
-      // Track that a file was selected
-      HopGuiSelectionTracker.getInstance()
-          
.setLastSelectionType(HopGuiSelectionTracker.SelectionType.FILE_EXPLORER);
     }
 
     boolean isFolderSelected = tif != null && tif.fileType instanceof 
FolderFileType;
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
index 49afd63d25..8f274956ca 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
@@ -156,8 +156,8 @@ public class MetadataPerspective implements 
IHopPerspective, TabClosable {
     return "metadata-perspective";
   }
 
-  @GuiKeyboardShortcut(control = true, shift = true, key = 'm')
-  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'm')
+  @GuiKeyboardShortcut(control = true, shift = true, key = 'm', global = true)
+  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'm', global = 
true)
   @Override
   public void activate() {
     hopGui.setActivePerspective(this);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/search/HopSearchPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/search/HopSearchPerspective.java
index 096c3d60d4..68d15b1d06 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/search/HopSearchPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/search/HopSearchPerspective.java
@@ -97,8 +97,8 @@ public class HopSearchPerspective implements IHopPerspective {
   }
 
   @Override
-  @GuiKeyboardShortcut(control = true, key = 'f')
-  @GuiOsxKeyboardShortcut(command = true, key = 'f')
+  @GuiKeyboardShortcut(control = true, key = 'f', global = true)
+  @GuiOsxKeyboardShortcut(command = true, key = 'f', global = true)
   public void activate() {
     // Someone clicked on the search icon of used CTRL-F
     //
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/selection/HopGuiSelectionTracker.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/selection/HopGuiSelectionTracker.java
deleted file mode 100644
index 151efe3e26..0000000000
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/selection/HopGuiSelectionTracker.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * 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.selection;
-
-import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Tracks the last selected item type to help route keyboard shortcuts 
correctly. This is needed
- * when multiple components (like file explorer and pipeline graph) share the 
same keyboard
- * shortcuts but need to act on different types of items.
- *
- * <p>Listeners can register to be notified when a given selection type is set 
(e.g. pipeline graph
- * selection changed).
- */
-public class HopGuiSelectionTracker {
-
-  private static HopGuiSelectionTracker instance;
-
-  /** The type of item that was last selected */
-  public enum SelectionType {
-    /** A file or folder in the file explorer */
-    FILE_EXPLORER,
-    /** A transform, note, or hop in a pipeline graph */
-    PIPELINE_GRAPH,
-    /** An action, note, or hop in a workflow graph */
-    WORKFLOW_GRAPH,
-    /** No specific selection */
-    NONE
-  }
-
-  private SelectionType lastSelectionType = SelectionType.NONE;
-
-  private final Map<SelectionType, List<Runnable>> selectionListeners =
-      new EnumMap<>(SelectionType.class);
-
-  private HopGuiSelectionTracker() {
-    for (SelectionType t : SelectionType.values()) {
-      selectionListeners.put(t, new ArrayList<>());
-    }
-  }
-
-  public static HopGuiSelectionTracker getInstance() {
-    if (instance == null) {
-      instance = new HopGuiSelectionTracker();
-    }
-    return instance;
-  }
-
-  /**
-   * Add a listener that runs when the given selection type is set.
-   *
-   * @param selectionType the type to listen for
-   * @param listener runnable to execute when 
setLastSelectionType(selectionType) is called
-   */
-  public void addSelectionListener(SelectionType selectionType, Runnable 
listener) {
-    selectionListeners.get(selectionType).add(listener);
-  }
-
-  /**
-   * Set the last selected item type and notify any listeners for this type.
-   *
-   * @param selectionType The type of item that was selected
-   */
-  public void setLastSelectionType(SelectionType selectionType) {
-    this.lastSelectionType = selectionType;
-    for (Runnable listener : selectionListeners.get(selectionType)) {
-      listener.run();
-    }
-  }
-
-  /**
-   * Get the last selected item type
-   *
-   * @return The type of item that was last selected
-   */
-  public SelectionType getLastSelectionType() {
-    return lastSelectionType;
-  }
-
-  /**
-   * Check if the last selection matches the given type
-   *
-   * @param selectionType The type to check against
-   * @return true if the last selection matches the given type
-   */
-  public boolean isLastSelection(SelectionType selectionType) {
-    return lastSelectionType == selectionType;
-  }
-}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiTerminalPanel.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiTerminalPanel.java
index 2929f8c841..023eb92eac 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiTerminalPanel.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiTerminalPanel.java
@@ -17,7 +17,12 @@
 
 package org.apache.hop.ui.hopgui.terminal;
 
+import lombok.Getter;
 import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+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;
 import org.apache.hop.history.AuditList;
 import org.apache.hop.history.AuditManager;
 import org.apache.hop.history.AuditState;
@@ -27,8 +32,10 @@ import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.HopNamespace;
 import org.apache.hop.ui.core.widget.TabFolderReorder;
 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.util.EnvironmentUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CTabFolder;
 import org.eclipse.swt.custom.CTabFolder2Adapter;
@@ -50,19 +57,22 @@ import org.eclipse.swt.widgets.ToolItem;
  * section and the terminal panel in the bottom section. The terminal panel 
persists across
  * perspective switches.
  */
+@GuiPlugin(name = "Terminal panel", description = "Terminal panel")
 public class HopGuiTerminalPanel 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";
+
   private final HopGui hopGui;
 
   private SashForm verticalSash;
-  private Composite perspectiveComposite;
+  @Getter private Composite perspectiveComposite;
   private Composite bottomPanelComposite;
   private Composite terminalComposite;
-  private CTabFolder terminalTabs;
+  @Getter private CTabFolder terminalTabs;
   private CTabItem newTerminalTab;
-
-  private boolean terminalVisible = false;
-  private int terminalHeightPercent = 35;
+  @Getter private boolean terminalVisible = false;
+  @Getter private int terminalHeightPercent = 35;
   private boolean isClearing = false;
   private int terminalCounter = 1;
 
@@ -106,6 +116,11 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     createBottomPanel();
 
     verticalSash.setMaximizedControl(perspectiveComposite);
+
+    // Register with key handler so Ctrl+J / Cmd+J and Ctrl+Shift+J / 
Cmd+Shift+J work in this panel
+    HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
+    keyHandler.addParentObjectToHandle(this);
+    hopGui.replaceKeyboardShortcutListeners(this, keyHandler);
   }
 
   /** Create the bottom panel with terminal */
@@ -311,7 +326,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     if (!terminalVisible) {
       verticalSash.setMaximizedControl(null);
       int perspectivePercent = 100 - terminalHeightPercent;
-      verticalSash.setWeights(new int[] {perspectivePercent, 
terminalHeightPercent});
+      verticalSash.setWeights(perspectivePercent, terminalHeightPercent);
       terminalVisible = true;
 
       if (terminalTabs.getItemCount() <= 1) {
@@ -322,11 +337,6 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     }
   }
 
-  /** Check if terminal panel is currently visible */
-  public boolean isTerminalVisible() {
-    return terminalVisible;
-  }
-
   /** Hide the terminal panel */
   public void hideTerminal() {
     if (terminalVisible) {
@@ -337,7 +347,17 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
   }
 
   /** Toggle terminal panel visibility */
+  @GuiMenuElement(
+      root = HopGui.ID_MAIN_MENU,
+      id = ID_MAIN_MENU_TOOLS_TERMINAL,
+      label = "i18n::HopGuiTerminalPanel.Menu.Terminal",
+      parentId = HopGui.ID_MAIN_MENU_TOOLS_PARENT_ID)
+  @GuiKeyboardShortcut(control = true, key = 'j', global = true)
+  @GuiOsxKeyboardShortcut(command = true, key = 'j', global = true)
   public void toggleTerminal() {
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      return;
+    }
     if (terminalVisible) {
       hideTerminal();
     } else {
@@ -345,6 +365,21 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     }
   }
 
+  /** Open a new terminal tab */
+  @GuiMenuElement(
+      root = HopGui.ID_MAIN_MENU,
+      id = ID_MAIN_MENU_TOOLS_NEW_TERMINAL,
+      label = "i18n::HopGuiTerminalPanel.Menu.NewTerminal",
+      parentId = HopGui.ID_MAIN_MENU_TOOLS_PARENT_ID)
+  @GuiKeyboardShortcut(control = true, shift = true, key = 'j', global = true)
+  @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'j', global = 
true)
+  public void newTerminal() {
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      return;
+    }
+    createNewTerminal(null, null);
+  }
+
   /** Close a terminal tab (implements TabClosable interface) */
   @Override
   public void closeTab(CTabFolderEvent event, CTabItem tabItem) {
@@ -443,7 +478,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
           } else {
             // Restore normal split
             verticalSash.setMaximizedControl(null);
-            verticalSash.setWeights(new int[] {100 - terminalHeightPercent, 
terminalHeightPercent});
+            verticalSash.setWeights(100 - terminalHeightPercent, 
terminalHeightPercent);
             
maximizeItem.setImage(GuiResource.getInstance().getImageMaximizePanel());
             maximizeItem.setToolTipText("Maximize terminal panel");
           }
@@ -670,29 +705,14 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     }
   }
 
-  /** Get the perspective composite */
-  public Composite getPerspectiveComposite() {
-    return perspectiveComposite;
-  }
-
-  /** Get the terminal tabs folder */
-  public CTabFolder getTerminalTabs() {
-    return terminalTabs;
-  }
-
   /** Set terminal height percentage */
   public void setTerminalHeightPercent(int percent) {
     if (percent > 0 && percent < 100) {
       this.terminalHeightPercent = percent;
       if (terminalVisible) {
         int perspectivePercent = 100 - terminalHeightPercent;
-        verticalSash.setWeights(new int[] {perspectivePercent, 
terminalHeightPercent});
+        verticalSash.setWeights(perspectivePercent, terminalHeightPercent);
       }
     }
   }
-
-  /** Get current terminal height percentage */
-  public int getTerminalHeightPercent() {
-    return terminalHeightPercent;
-  }
 }
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
new file mode 100644
index 0000000000..672d4d683f
--- /dev/null
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/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.
+#
+#
+
+HopGuiTerminalPanel.Menu.Terminal=Terminal
+HopGuiTerminalPanel.Menu.NewTerminal=New Terminal

Reply via email to