From 8d4fba7786bb8ff47b2de569bf152269e1162da8 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sun, 19 Jul 2026 09:24:50 +0300 Subject: [PATCH] =?UTF-8?q?Fixed=20Ctrl++/=E2=88=92=20zoom=20shortcuts=20n?= =?UTF-8?q?ot=20working=20with=20the=20numpad=20+/=E2=88=92=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bound VK_ADD/VK_SUBTRACT/VK_NUMPAD0 alongside the existing main-keyboard VK_EQUALS/VK_MINUS/VK_0 in ZoomSupport.bindZoomKeys() (used by Markdown Preview and the terminal tab's Swing InputMap) and in ClaudeSessionTab.zoomKeyInterceptor (the global KeyEventDispatcher needed because JediTerm consumes key events before Swing's InputMap sees them). Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + pom.xml | 2 +- .../claudecodegui/ui/ClaudeSessionTab.java | 9 ++- .../claudecodegui/ui/common/ZoomSupport.java | 9 ++- .../ui/ClaudeSessionTabZoomKeyTest.java | 65 +++++++++++++++++++ .../ui/common/ZoomSupportTest.java | 34 ++++++++++ 6 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 src/test/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTabZoomKeyTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c96746b..24bde980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed Ctrl++/− terminal and Markdown Preview zoom shortcuts not working when using the numpad +/− keys - Renamed the "Subscription" connection type to "Claude Subscription" in Profile settings, to distinguish it from the new "ChatGPT Subscription" connection type - Added support for signing in with a ChatGPT Plus/Pro/Team subscription as a new profile connection type, routing sessions through OpenAI's Codex backend instead of requiring an OpenAI API key; click "Copy sign in link" to copy the sign-in URL, open it in your own browser, then paste the resulting code back to complete sign-in — the plugin never opens a browser or waits on it automatically. Once signed in, "Model Aliases…" is available for this profile and fetches the live list of models available to your account instead of a fixed list; if the fetch fails, the upstream error is shown in the dialog's status line diff --git a/pom.xml b/pom.xml index 71b5ba21..c948cd18 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.3.18-SNAPSHOT + 1.3.19-SNAPSHOT nbm Netbeans Plugin Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTab.java index 767bdc89..22eda07e 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTab.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTab.java @@ -191,9 +191,12 @@ public class ClaudeSessionTab extends TopComponent boolean ctrl = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; if (!ctrl) return false; switch (e.getKeyCode()) { - case KeyEvent.VK_0: SwingUtilities.invokeLater(this::resetZoom); return true; - case KeyEvent.VK_MINUS: SwingUtilities.invokeLater(this::zoomOut); return true; - case KeyEvent.VK_EQUALS: SwingUtilities.invokeLater(this::zoomIn); return true; + case KeyEvent.VK_0: + case KeyEvent.VK_NUMPAD0: SwingUtilities.invokeLater(this::resetZoom); return true; + case KeyEvent.VK_MINUS: + case KeyEvent.VK_SUBTRACT: SwingUtilities.invokeLater(this::zoomOut); return true; + case KeyEvent.VK_EQUALS: + case KeyEvent.VK_ADD: SwingUtilities.invokeLater(this::zoomIn); return true; default: return false; } }; diff --git a/src/main/java/io/github/nbplugins/claudecodegui/ui/common/ZoomSupport.java b/src/main/java/io/github/nbplugins/claudecodegui/ui/common/ZoomSupport.java index 409fae58..2a7fa1c7 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/ui/common/ZoomSupport.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/ui/common/ZoomSupport.java @@ -138,9 +138,12 @@ public static void appendZoomMenu(JPopupMenu menu, Zoomable zoomable) { * populated so the shortcuts work whether the component itself or a child has focus. */ public static void bindZoomKeys(JComponent comp, Zoomable zoomable) { - bindKey(comp, KeyEvent.VK_0, "zoom-reset", zoomable::resetZoom); - bindKey(comp, KeyEvent.VK_MINUS, "zoom-out", zoomable::zoomOut); - bindKey(comp, KeyEvent.VK_EQUALS, "zoom-in", zoomable::zoomIn); + bindKey(comp, KeyEvent.VK_0, "zoom-reset", zoomable::resetZoom); + bindKey(comp, KeyEvent.VK_NUMPAD0, "zoom-reset", zoomable::resetZoom); + bindKey(comp, KeyEvent.VK_MINUS, "zoom-out", zoomable::zoomOut); + bindKey(comp, KeyEvent.VK_SUBTRACT, "zoom-out", zoomable::zoomOut); + bindKey(comp, KeyEvent.VK_EQUALS, "zoom-in", zoomable::zoomIn); + bindKey(comp, KeyEvent.VK_ADD, "zoom-in", zoomable::zoomIn); } /** Returns a MouseAdapter that resets zoom on Alt+middle-click (button 2). */ diff --git a/src/test/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTabZoomKeyTest.java b/src/test/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTabZoomKeyTest.java new file mode 100644 index 00000000..fc4e2d07 --- /dev/null +++ b/src/test/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTabZoomKeyTest.java @@ -0,0 +1,65 @@ +package io.github.nbplugins.claudecodegui.ui; + +import java.awt.KeyEventDispatcher; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.lang.reflect.Field; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link ClaudeSessionTab}'s {@code zoomKeyInterceptor}, the + * {@code KeyEventDispatcher} that handles Ctrl+zoom shortcuts while focus is + * inside the embedded JediTerm widget (which otherwise consumes key events + * before Swing's InputMap machinery sees them). + */ +class ClaudeSessionTabZoomKeyTest { + + @SuppressWarnings("unchecked") + private static KeyEventDispatcher getInterceptor(ClaudeSessionTab tab) throws Exception { + Field f = ClaudeSessionTab.class.getDeclaredField("zoomKeyInterceptor"); + f.setAccessible(true); + return (KeyEventDispatcher) f.get(tab); + } + + private static void setTerminalWidget(ClaudeSessionTab tab, ZoomableJediTermWidget widget) throws Exception { + Field f = ClaudeSessionTab.class.getDeclaredField("terminalWidget"); + f.setAccessible(true); + f.set(tab, widget); + } + + private static KeyEvent keyPress(java.awt.Component src, int keyCode) { + return new KeyEvent(src, KeyEvent.KEY_PRESSED, 0L, InputEvent.CTRL_DOWN_MASK, + keyCode, KeyEvent.CHAR_UNDEFINED); + } + + @Test + void dispatchKeyEvent_ctrlNumpadPlusAndMinusTriggerZoom() throws Exception { + ClaudeSessionTab tab = new ClaudeSessionTab(); + ZoomableJediTermWidget widget = new ZoomableJediTermWidget(new NetBeansSettingsProvider(), tab); + setTerminalWidget(tab, widget); + KeyEventDispatcher interceptor = getInterceptor(tab); + + int deltaBefore = tab.getZoomDelta(); + + assertTrue(interceptor.dispatchKeyEvent(keyPress(widget, KeyEvent.VK_ADD)), + "Ctrl+NumpadPlus must be consumed"); + java.awt.Toolkit.getDefaultToolkit().sync(); + waitForEdt(); + assertEquals(deltaBefore + 1, tab.getZoomDelta(), "Ctrl+NumpadPlus must zoom in"); + + assertTrue(interceptor.dispatchKeyEvent(keyPress(widget, KeyEvent.VK_SUBTRACT)), + "Ctrl+NumpadMinus must be consumed"); + waitForEdt(); + assertEquals(deltaBefore, tab.getZoomDelta(), "Ctrl+NumpadMinus must zoom back out"); + + assertTrue(interceptor.dispatchKeyEvent(keyPress(widget, KeyEvent.VK_NUMPAD0)), + "Ctrl+Numpad0 must be consumed"); + waitForEdt(); + assertEquals(0, tab.getZoomDelta(), "Ctrl+Numpad0 must reset zoom"); + } + + private static void waitForEdt() throws Exception { + javax.swing.SwingUtilities.invokeAndWait(() -> {}); + } +} diff --git a/src/test/java/io/github/nbplugins/claudecodegui/ui/common/ZoomSupportTest.java b/src/test/java/io/github/nbplugins/claudecodegui/ui/common/ZoomSupportTest.java index ef4529f8..47c5acd5 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/ui/common/ZoomSupportTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/ui/common/ZoomSupportTest.java @@ -251,6 +251,40 @@ void bindZoomKeys_registersCtrl0CtrlMinusCtrlEqualsInBothMaps() { assertEquals(1, ins[0]); } + @Test + void bindZoomKeys_registersNumpadPlusMinusAndZeroInBothMaps() { + int[] resets = {0}, outs = {0}, ins = {0}; + Zoomable z = new Zoomable() { + @Override public void zoomIn() { ins[0]++; } + @Override public void zoomOut() { outs[0]++; } + @Override public void resetZoom() { resets[0]++; } + @Override public int getZoomDelta() { return 0; } + @Override public int getMinDelta() { return -8; } + @Override public int getMaxDelta() { return 20; } + }; + JLabel comp = new JLabel(); + ZoomSupport.bindZoomKeys(comp, z); + + int ctrl = InputEvent.CTRL_DOWN_MASK; + KeyStroke ctrlNumpad0 = KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0, ctrl); + KeyStroke ctrlNumMinus = KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, ctrl); + KeyStroke ctrlNumPlus = KeyStroke.getKeyStroke(KeyEvent.VK_ADD, ctrl); + + for (int map : new int[]{JComponent.WHEN_FOCUSED, JComponent.WHEN_IN_FOCUSED_WINDOW}) { + assertEquals("zoom-reset", comp.getInputMap(map).get(ctrlNumpad0)); + assertEquals("zoom-out", comp.getInputMap(map).get(ctrlNumMinus)); + assertEquals("zoom-in", comp.getInputMap(map).get(ctrlNumPlus)); + } + + comp.getActionMap().get("zoom-reset").actionPerformed(null); + comp.getActionMap().get("zoom-out").actionPerformed(null); + comp.getActionMap().get("zoom-in").actionPerformed(null); + + assertEquals(1, resets[0]); + assertEquals(1, outs[0]); + assertEquals(1, ins[0]); + } + // --- createClickListener --------------------------------------------------- private static MouseEvent mouseButton(java.awt.Component src, int button, int modifiers) {