This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new e1aed0bc7804 CAMEL-23869: Add mouse support to browser popups and fix 
MemoryLeakTab crash
e1aed0bc7804 is described below

commit e1aed0bc780452846b3c24231fbbbc9f77a092fe
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 5 17:48:13 2026 +0200

    CAMEL-23869: Add mouse support to browser popups and fix MemoryLeakTab crash
    
    Add scroll wheel and click-to-select mouse support to ExampleBrowserPopup
    and InfraBrowserPopup. Track per-item line heights for accurate click 
mapping
    on multi-line wrapped descriptions. Fix modal popup mouse capture so tab bar
    clicks don't leak through when F2 popup is open. Escape literal % signs in
    MemoryLeakTab help text to prevent UnknownFormatConversionException.
    
    CAMEL-23869: Add Switch Integration to F2 actions menu in camel-tui
    
    Add Switch Integration (F3) entry to the F2 actions menu so the feature
    is discoverable. The entry is dimmed when fewer than 2 integrations are
    running, matching the F3 shortcut behavior.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  | 38 +++++++++++++--
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 10 ++--
 .../core/commands/tui/ExampleBrowserPopup.java     | 55 +++++++++++++++++++++
 .../jbang/core/commands/tui/InfraBrowserPopup.java | 56 ++++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/MemoryLeakTab.java | 14 +++---
 5 files changed, 159 insertions(+), 14 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 2e87ca17eccf..f1c64d419b77 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -51,6 +51,7 @@ class ActionsPopup {
 
     enum Action {
         GOTO_TAB,
+        SWITCH_INTEGRATION,
         SEND_MESSAGE,
         RUN_EXAMPLE,
         RUN_FOLDER,
@@ -73,7 +74,7 @@ class ActionsPopup {
         SHELL
     }
 
-    private static final int[] GROUP_SIZES = { 1, 6, 4, 5 };
+    private static final int[] GROUP_SIZES = { 2, 6, 4, 5 };
     private static final int MCP_GROUP_SIZE = 4;
     private static final int SHELL_GROUP_SIZE = 1;
 
@@ -89,6 +90,7 @@ class ActionsPopup {
     private Runnable resetScreenAction;
     private Runnable openShellAction;
     private Runnable browseFilesAction;
+    private Runnable switchIntegrationAction;
     private final Supplier<Boolean> tapeRecordingActive;
     private MonitorContext ctx;
     private boolean mcpEnabled;
@@ -181,6 +183,10 @@ class ActionsPopup {
         this.browseFilesAction = browseFilesAction;
     }
 
+    void setSwitchIntegrationAction(Runnable switchIntegrationAction) {
+        this.switchIntegrationAction = switchIntegrationAction;
+    }
+
     void setGotoTabSupport(List<TabRegistry.TabEntry> entries, Runnable 
callback) {
         gotoTabPopup.setTabEntries(entries, callback);
     }
@@ -248,6 +254,7 @@ class ActionsPopup {
     private List<Action> buildVisualActionList() {
         List<Action> flat = new ArrayList<>();
         flat.add(Action.GOTO_TAB);
+        flat.add(Action.SWITCH_INTEGRATION);
         flat.add(null);
         flat.addAll(List.of(
                 Action.SEND_MESSAGE, Action.RUN_EXAMPLE, Action.RUN_FOLDER, 
Action.RUN_INFRA, Action.BROWSE_FILES,
@@ -338,8 +345,9 @@ class ActionsPopup {
 
     List<String> getActionLabels() {
         List<String> labels = new ArrayList<>();
-        // Group 0: Go to
+        // Group 0: Navigation
         labels.add("Go to...");
+        labels.add("Switch Integration (F3)");
         labels.add("───");
         // Group 1: User Actions
         labels.add("Send Message");
@@ -524,6 +532,13 @@ class ActionsPopup {
                     } else if (action == Action.GOTO_TAB) {
                         showActionsMenu = false;
                         gotoTabPopup.open();
+                    } else if (action == Action.SWITCH_INTEGRATION) {
+                        if (hasMultipleIntegrations()) {
+                            showActionsMenu = false;
+                            if (switchIntegrationAction != null) {
+                                switchIntegrationAction.run();
+                            }
+                        }
                     } else if (action == Action.SHELL) {
                         showActionsMenu = false;
                         if (openShellAction != null) {
@@ -624,11 +639,17 @@ class ActionsPopup {
         if (showActionsMenu) {
             return handleListPopupMouse(me, actionsMenuRect, actionsMenuState, 
visualActionCount(), this::isDividerIndex);
         }
+        if (exampleBrowserPopup.isVisible()) {
+            return exampleBrowserPopup.handleMouseEvent(me);
+        }
+        if (infraBrowserPopup.isBrowserVisible()) {
+            return infraBrowserPopup.handleMouseEvent(me);
+        }
         if (docViewerPopup.isPickerVisible() && 
docViewerPopup.getPickerIntegrations() != null) {
             return handleListPopupMouse(me, docViewerPopup.getPickerRect(), 
docViewerPopup.getPickerState(),
                     docViewerPopup.getPickerIntegrations().size(), i -> false);
         }
-        // Other sub-popups (forms, browsers, viewers) stay modal: consume the 
event without acting on it.
+        // Other sub-popups (forms, viewers) stay modal: consume the event 
without acting on it.
         return true;
     }
 
@@ -798,9 +819,13 @@ class ActionsPopup {
                 ? TuiIcons.menuItem(TuiIcons.STOP, "Stop Tape Recording 
(Ctrl+R)")
                 : TuiIcons.menuItem(TuiIcons.RECORD, "Start Tape Recording 
(Ctrl+R)");
 
+        boolean canSwitch = hasMultipleIntegrations();
         List<ListItem> items = new ArrayList<>();
-        // Group 0: Go to
+        // Group 0: Navigation
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.GO_TO, "Go 
to...")));
+        items.add(canSwitch
+                ? ListItem.from(TuiIcons.menuItem(TuiIcons.ARROW_BOTH, "Switch 
Integration (F3)"))
+                : ListItem.from(TuiIcons.menuItem(TuiIcons.ARROW_BOTH, "Switch 
Integration (F3)")).style(Style.EMPTY.dim()));
         items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
         // Group 1: User Actions
         boolean hasSelection = ctx != null && ctx.selectedPid != null && 
!ctx.isInfraSelected();
@@ -854,6 +879,11 @@ class ActionsPopup {
         frame.renderStatefulWidget(list, popup, actionsMenuState);
     }
 
+    private boolean hasMultipleIntegrations() {
+        List<IntegrationInfo> ints = integrations.get();
+        return ints != null && ints.stream().filter(i -> !i.vanishing && i.pid 
!= null).count() > 1;
+    }
+
     void openDoc(IntegrationInfo info) {
         showActionsMenu = false;
         String error = docViewerPopup.openDoc(info);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 65a5db83a6f4..2e37d0bb2374 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -252,6 +252,8 @@ public class CamelMonitor extends CamelCommand {
         aiPanel.setContext(ctx);
         actionsPopup.setOpenShellAction(shellPanel::open);
         actionsPopup.setBrowseFilesAction(this::openFilesPopup);
+        actionsPopup.setSwitchIntegrationAction(
+                () -> popupManager.openSwitchPopup(ctx.selectedPid, 
getNonVanishingIntegrations()));
 
         tabRegistry = new TabRegistry(tabsState);
         tabRegistry.initTabs(ctx, dataService, this::resetIntegrationTabState);
@@ -797,6 +799,11 @@ public class CamelMonitor extends CamelCommand {
             return true;
         }
 
+        // Modal popups capture all mouse events (including tab bar and footer)
+        if (actionsPopup.isVisible()) {
+            return actionsPopup.handleMouseEvent(me);
+        }
+
         // Tab bar clicks: detect which tab was clicked and switch to it
         if (me.isClick() && lastTabsArea != null && lastTabLabels != null) {
             int tabsY = lastTabsArea.height() >= 2 ? lastTabsArea.y() + 1 : 
lastTabsArea.y();
@@ -825,9 +832,6 @@ public class CamelMonitor extends CamelCommand {
             if (popupManager.isMorePopupVisible() || 
popupManager.isSwitchPopupVisible()) {
                 return popupManager.handleMouseEvent(me, 
tabRegistry.selectedTabIndex(), TAB_LOG);
             }
-            if (actionsPopup.isVisible()) {
-                return actionsPopup.handleMouseEvent(me);
-            }
             if (filesBrowser.isVisible()) {
                 return false;
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
index dcbae7e6cdf2..9baded611241 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
@@ -30,6 +30,8 @@ import dev.tamboui.text.Span;
 import dev.tamboui.text.Text;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.tui.event.MouseEventKind;
 import dev.tamboui.widgets.Clear;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
@@ -46,6 +48,8 @@ class ExampleBrowserPopup {
 
     private boolean visible;
     private final ListState listState = new ListState();
+    private Rect popupRect;
+    private int[] itemHeights;
     private List<JsonObject> catalog;
     private JsonObject selectedExample;
 
@@ -132,6 +136,49 @@ class ExampleBrowserPopup {
         return true;
     }
 
+    boolean handleMouseEvent(MouseEvent me) {
+        if (me.kind() == MouseEventKind.SCROLL_UP) {
+            navigate(-1);
+            return true;
+        }
+        if (me.kind() == MouseEventKind.SCROLL_DOWN) {
+            navigate(1);
+            return true;
+        }
+        if (me.isClick()) {
+            if (popupRect != null && popupRect.contains(me.x(), me.y())) {
+                int idx = itemAtMouseY(me.y());
+                if (idx >= 0 && !isSeparatorIndex(idx)) {
+                    listState.select(idx);
+                }
+                return true;
+            }
+            close();
+            return true;
+        }
+        return true;
+    }
+
+    private int itemAtMouseY(int mouseY) {
+        if (popupRect == null || itemHeights == null) {
+            return -1;
+        }
+        int firstRow = popupRect.top() + 1;
+        int relY = mouseY - firstRow;
+        if (relY < 0) {
+            return -1;
+        }
+        int offset = listState.offset();
+        int rowAcc = 0;
+        for (int i = offset; i < itemHeights.length; i++) {
+            rowAcc += itemHeights[i];
+            if (relY < rowAcc) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     void render(Frame frame, Rect area) {
         if (catalog == null || catalog.isEmpty()) {
             return;
@@ -141,6 +188,7 @@ class ExampleBrowserPopup {
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
         int y = area.top() + 2;
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
+        this.popupRect = popup;
 
         frame.renderWidget(Clear.INSTANCE, popup);
 
@@ -389,6 +437,7 @@ class ExampleBrowserPopup {
 
     private List<ListItem> buildListItems(int width) {
         List<ListItem> items = new ArrayList<>();
+        List<Integer> heights = new ArrayList<>();
         String currentLevel = null;
         for (JsonObject ex : catalog) {
             String level = ex.getStringOrDefault("level", "beginner");
@@ -396,6 +445,7 @@ class ExampleBrowserPopup {
                 currentLevel = level;
                 String header = "── " + TuiHelper.capitalize(level) + " ──";
                 items.add(ListItem.from(header).style(Style.EMPTY.dim()));
+                heights.add(1);
             }
             String name = ex.getStringOrDefault("name", "");
             String desc = ex.getStringOrDefault("description", "");
@@ -414,6 +464,7 @@ class ExampleBrowserPopup {
             Style style = bundled ? Style.EMPTY : Style.EMPTY.dim();
             if (desc.length() <= descCol) {
                 items.add(ListItem.from(prefix + desc).style(style));
+                heights.add(1);
             } else {
                 String indent = " ".repeat(prefix.length());
                 List<Line> lines = new ArrayList<>();
@@ -423,13 +474,17 @@ class ExampleBrowserPopup {
                     lines.add(Line.from(indent + wrapped.get(w)));
                 }
                 
items.add(ListItem.from(Text.from(lines.toArray(Line[]::new))).style(style));
+                heights.add(wrapped.size());
             }
         }
         items.add(ListItem.from(""));
+        heights.add(1);
         items.add(ListItem.from(" " + TuiIcons.BUNDLED + " = bundled  " + 
TuiIcons.ONLINE + " = online  "
                                 + TuiIcons.DOCKER + " = Docker  " + 
TuiIcons.INFRA + " = infra services  " + TuiIcons.CITRUS
                                 + " = Citrus tests")
                 .style(Style.EMPTY.dim()));
+        heights.add(1);
+        this.itemHeights = 
heights.stream().mapToInt(Integer::intValue).toArray();
         return items;
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java
index 4ac21e8b4d85..b52eb115f61e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java
@@ -37,6 +37,8 @@ import dev.tamboui.text.Span;
 import dev.tamboui.text.Text;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.tui.event.MouseEventKind;
 import dev.tamboui.widgets.Clear;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
@@ -63,6 +65,8 @@ class InfraBrowserPopup {
     private boolean showBrowser;
     private boolean showPortDialog;
     private final ListState browserState = new ListState();
+    private Rect popupRect;
+    private int[] itemHeights;
     private List<InfraServiceEntry> catalog;
     private InfraServiceEntry selectedService;
     private int implIndex;
@@ -132,6 +136,52 @@ class InfraBrowserPopup {
         return false;
     }
 
+    boolean handleMouseEvent(MouseEvent me) {
+        if (!showBrowser) {
+            return true;
+        }
+        if (me.kind() == MouseEventKind.SCROLL_UP) {
+            navigate(-1);
+            return true;
+        }
+        if (me.kind() == MouseEventKind.SCROLL_DOWN) {
+            navigate(1);
+            return true;
+        }
+        if (me.isClick()) {
+            if (popupRect != null && popupRect.contains(me.x(), me.y())) {
+                int idx = itemAtMouseY(me.y());
+                if (idx >= 0 && catalog != null && idx < catalog.size() && 
!catalog.get(idx).running()) {
+                    browserState.select(idx);
+                }
+                return true;
+            }
+            close();
+            return true;
+        }
+        return true;
+    }
+
+    private int itemAtMouseY(int mouseY) {
+        if (popupRect == null || itemHeights == null) {
+            return -1;
+        }
+        int firstRow = popupRect.top() + 1;
+        int relY = mouseY - firstRow;
+        if (relY < 0) {
+            return -1;
+        }
+        int offset = browserState.offset();
+        int rowAcc = 0;
+        for (int i = offset; i < itemHeights.length; i++) {
+            rowAcc += itemHeights[i];
+            if (relY < rowAcc) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     void render(Frame frame, Rect area) {
         if (showBrowser) {
             renderBrowser(frame, area);
@@ -281,16 +331,19 @@ class InfraBrowserPopup {
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
         int y = area.top() + 2;
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
+        this.popupRect = popup;
 
         frame.renderWidget(Clear.INSTANCE, popup);
 
         int nameCol = 22;
         List<ListItem> items = new ArrayList<>();
+        List<Integer> heights = new ArrayList<>();
         for (InfraServiceEntry entry : catalog) {
             String padded = String.format("%-" + nameCol + "s", 
TuiHelper.truncate(entry.alias(), nameCol));
             String prefix = TuiIcons.indent(TuiIcons.INFRA) + padded + " ";
             if (entry.running()) {
                 items.add(ListItem.from(prefix + 
"(running)").style(Style.EMPTY.dim()));
+                heights.add(1);
             } else {
                 String implStr = entry.implementations().isEmpty() ? "" : 
String.join(", ", entry.implementations());
                 String desc = entry.description();
@@ -300,6 +353,7 @@ class InfraBrowserPopup {
                 int descW = Math.max(10, popupW - prefix.length() - 2);
                 if (desc.length() <= descW) {
                     items.add(ListItem.from(prefix + desc));
+                    heights.add(1);
                 } else {
                     String indent = " ".repeat(prefix.length());
                     List<Line> lines = new ArrayList<>();
@@ -309,9 +363,11 @@ class InfraBrowserPopup {
                         lines.add(Line.from(indent + wrapped.get(w)));
                     }
                     
items.add(ListItem.from(Text.from(lines.toArray(Line[]::new))));
+                    heights.add(wrapped.size());
                 }
             }
         }
+        this.itemHeights = 
heights.stream().mapToInt(Integer::intValue).toArray();
 
         long available = catalog.stream().filter(e -> !e.running()).count();
         ListWidget list = ListWidget.builder()
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
index f2d6be86c8f4..de08309ea008 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
@@ -923,17 +923,17 @@ class MemoryLeakTab extends AbstractTab {
 
                 After both complete, a comparison table shows how each class
                 behaved across the two runs. The **GROWTH** column shows the
-                normalized growth as a percentage. For example, +30% means
-                the class grew 30% faster than expected from the duration
+                normalized growth as a percentage. For example, +30%% means
+                the class grew 30%% faster than expected from the duration
                 increase alone. Entries under 1KB in both runs are filtered
                 out as noise.
 
                 ### Trend Indicators
 
-                - **↑ leak!** (red) — Growth >= +20%, very likely leak
-                - **↑ leak?** (yellow) — Growth +10% to +20%, suspicious
-                - **→ stable** (green) — Growth -20% to +10%, normal
-                - **↓** (dim) — Growth < -20%, shrinking
+                - **↑ leak!** (red) — Growth >= +20%%, very likely leak
+                - **↑ leak?** (yellow) — Growth +10%% to +20%%, suspicious
+                - **→ stable** (green) — Growth -20%% to +10%%, normal
+                - **↓** (dim) — Growth < -20%%, shrinking
                 - **new** (yellow) — Only appeared in Run 2
                 - **gone** (dim) — Only appeared in Run 1
 
@@ -942,7 +942,7 @@ class MemoryLeakTab extends AbstractTab {
                 A **%s** warning appears when sample counts are too low
                 (fewer than 5 in either run) or diverge significantly from
                 the expected duration ratio. The growth percentage is shown
-                with a **~** prefix (e.g. ~+53%) to indicate the value may
+                with a **~** prefix (e.g. ~+53%%) to indicate the value may
                 not be reliable. JFR sampling is statistical — low sample
                 counts produce noisy results. Re-run with a longer duration
                 to collect more samples.

Reply via email to