diff --git a/CHANGELOG.md b/CHANGELOG.md index 92544b68..716855cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ - Fixed F5 not refreshing the Markdown Preview from disk -- Added font zoom (Alt+Scroll) for Terminal and Markdown Preview; Ctrl+0 resets; right-click "Zoom" submenu +- Added font zoom (Alt+Scroll / Ctrl+= / Ctrl+−) for Terminal and Markdown Preview; Ctrl+0 or Alt+Click Wheel resets; right-click "Zoom" submenu - Added Find bar (Ctrl+F / F3 / Shift+F3) to Markdown Preview - Added automatic Markdown Preview tab when Claude writes a plan file (configurable in Settings → General) - Added `show_markdown` MCP tool: displays markdown text in a NetBeans Markdown Preview tab diff --git a/pom.xml b/pom.xml index 1c3659a7..93bc4b32 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.23.18-SNAPSHOT + 0.23.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/nbclaudecodegui/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java index f73b519a..74cde8d2 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java @@ -188,12 +188,14 @@ public class ClaudeSessionTab extends TopComponent private final KeyEventDispatcher zoomKeyInterceptor = e -> { if (e.getID() != KeyEvent.KEY_PRESSED || terminalWidget == null) return false; if (!SwingUtilities.isDescendingFrom(e.getComponent(), terminalWidget)) return false; - if (e.getKeyCode() == KeyEvent.VK_0 - && (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0) { - SwingUtilities.invokeLater(this::resetZoom); - return true; + 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; + default: return false; } - return false; }; private JSplitPane splitPane; private JPanel errorPanel; @@ -904,9 +906,10 @@ public void componentResized(ComponentEvent e) { widget.getTerminalPanel().addMouseWheelListener( ZoomSupport.createWheelListener(this)); + widget.getTerminalPanel().addMouseListener(ZoomSupport.createClickListener(this)); // The Zoom submenu is spliced into JediTerm's native context menu by // ZoomableJediTermWidget (JediTerm ignores setComponentPopupMenu). - ZoomSupport.bindResetKey(this, this); + ZoomSupport.bindZoomKeys(this, this); add(splitPane, BorderLayout.CENTER); revalidate(); diff --git a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java index 529e1611..70d8aeeb 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java @@ -174,7 +174,8 @@ public static void openLive(String filePath, String content, FileObject fo) { tab.pane.setComponentPopupMenu(tab.buildContextMenu()); tab.initFindBar(tab.pane); tab.pane.addMouseWheelListener(ZoomSupport.createWheelListener(tab, true)); - ZoomSupport.bindResetKey(tab.pane, tab); + tab.pane.addMouseListener(ZoomSupport.createClickListener(tab)); + ZoomSupport.bindZoomKeys(tab.pane, tab); tab.applyMdZoom(tab.mdZoomDelta); if (fo != null) { @@ -704,7 +705,8 @@ protected void componentOpened() { pane.setComponentPopupMenu(buildContextMenu()); initFindBar(pane); pane.addMouseWheelListener(ZoomSupport.createWheelListener(this, true)); - ZoomSupport.bindResetKey(pane, this); + pane.addMouseListener(ZoomSupport.createClickListener(this)); + ZoomSupport.bindZoomKeys(pane, this); applyMdZoom(mdZoomDelta); } diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/ZoomSupport.java b/src/main/java/io/github/nbclaudecodegui/ui/common/ZoomSupport.java index 397cef9d..9319e7fa 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/common/ZoomSupport.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/common/ZoomSupport.java @@ -4,6 +4,8 @@ import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import javax.swing.AbstractAction; @@ -92,13 +94,13 @@ private static void forwardToScrollAncestor(MouseWheelEvent e) { public static JMenu buildZoomMenu(Zoomable zoomable) { JMenu menu = new JMenu("Zoom"); - JMenuItem increase = new JMenuItem("Increase (Alt+Scroll Up)"); + JMenuItem increase = new JMenuItem("Increase (Alt+Scroll Up / Ctrl+=)"); increase.addActionListener(e -> zoomable.zoomIn()); - JMenuItem decrease = new JMenuItem("Decrease (Alt+Scroll Down)"); + JMenuItem decrease = new JMenuItem("Decrease (Alt+Scroll Down / Ctrl+−)"); decrease.addActionListener(e -> zoomable.zoomOut()); - JMenuItem reset = new JMenuItem("Reset (Ctrl+0)"); + JMenuItem reset = new JMenuItem("Reset (Ctrl+0 / Alt+Click Wheel)"); reset.addActionListener(e -> zoomable.resetZoom()); menu.add(increase); @@ -130,14 +132,36 @@ public static void appendZoomMenu(JPopupMenu menu, Zoomable zoomable) { menu.add(buildZoomMenu(zoomable)); } - /** Binds Ctrl+0 → resetZoom() on the given component via InputMap/ActionMap. */ - public static void bindResetKey(JComponent comp, Zoomable zoomable) { - KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_0, InputEvent.CTRL_DOWN_MASK); - // WHEN_FOCUSED overrides the NetBeans global Ctrl+0 action while comp is focused - comp.getInputMap(JComponent.WHEN_FOCUSED).put(ks, "zoom-reset"); - comp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ks, "zoom-reset"); - comp.getActionMap().put("zoom-reset", new AbstractAction() { - @Override public void actionPerformed(ActionEvent e) { zoomable.resetZoom(); } + /** + * Binds Ctrl+0 (reset), Ctrl+− (out), and Ctrl+= (in) on the given component + * via InputMap/ActionMap. Both WHEN_FOCUSED and WHEN_IN_FOCUSED_WINDOW maps are + * 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); + } + + /** Returns a MouseAdapter that resets zoom on Alt+middle-click (button 2). */ + public static MouseAdapter createClickListener(Zoomable zoomable) { + return new MouseAdapter() { + @Override public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON2 + && (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0) { + e.consume(); + zoomable.resetZoom(); + } + } + }; + } + + private static void bindKey(JComponent comp, int vk, String actionKey, Runnable action) { + KeyStroke ks = KeyStroke.getKeyStroke(vk, InputEvent.CTRL_DOWN_MASK); + comp.getInputMap(JComponent.WHEN_FOCUSED).put(ks, actionKey); + comp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ks, actionKey); + comp.getActionMap().put(actionKey, new AbstractAction() { + @Override public void actionPerformed(ActionEvent e) { action.run(); } }); } } diff --git a/src/test/java/io/github/nbclaudecodegui/ui/common/ZoomSupportTest.java b/src/test/java/io/github/nbclaudecodegui/ui/common/ZoomSupportTest.java index 730cccf6..724958a1 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/common/ZoomSupportTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/common/ZoomSupportTest.java @@ -2,6 +2,8 @@ import java.awt.event.InputEvent; import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import javax.swing.Action; @@ -213,11 +215,51 @@ void createWheelListener_forwardTrue_altWheelStillZoomsAndIsNotForwarded() { assertTrue(alt.isConsumed(), "zoom event must be consumed"); } - // --- bindResetKey: Ctrl+0 must reach the focused surface (NetBeans steals - // WHEN_IN_FOCUSED_WINDOW), so it must also be in WHEN_FOCUSED ----------- + // --- bindZoomKeys: all three shortcuts registered in both input maps -------- @Test - void bindResetKey_registersCtrl0InFocusedAndWindowMaps() { + void bindZoomKeys_registersCtrl0CtrlMinusCtrlEqualsInBothMaps() { + 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 ctrl0 = KeyStroke.getKeyStroke(KeyEvent.VK_0, ctrl); + KeyStroke ctrlMinus = KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, ctrl); + KeyStroke ctrlEqual = KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, ctrl); + + for (int map : new int[]{JComponent.WHEN_FOCUSED, JComponent.WHEN_IN_FOCUSED_WINDOW}) { + assertEquals("zoom-reset", comp.getInputMap(map).get(ctrl0)); + assertEquals("zoom-out", comp.getInputMap(map).get(ctrlMinus)); + assertEquals("zoom-in", comp.getInputMap(map).get(ctrlEqual)); + } + + 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) { + return new MouseEvent(src, MouseEvent.MOUSE_PRESSED, 0L, + modifiers, 0, 0, 1, false, button); + } + + @Test + void createClickListener_altMiddleClickResetsZoom() { int[] resets = {0}; Zoomable z = new Zoomable() { @Override public void zoomIn() {} @@ -227,19 +269,43 @@ void bindResetKey_registersCtrl0InFocusedAndWindowMaps() { @Override public int getMinDelta() { return -8; } @Override public int getMaxDelta() { return 20; } }; - JLabel comp = new JLabel(); + MouseAdapter listener = ZoomSupport.createClickListener(z); + listener.mousePressed(mouseButton(new JLabel(), MouseEvent.BUTTON2, + InputEvent.ALT_DOWN_MASK | InputEvent.getMaskForButton(MouseEvent.BUTTON2))); + assertEquals(1, resets[0]); + } - ZoomSupport.bindResetKey(comp, z); - - KeyStroke ctrl0 = KeyStroke.getKeyStroke(KeyEvent.VK_0, InputEvent.CTRL_DOWN_MASK); - assertEquals("zoom-reset", - comp.getInputMap(JComponent.WHEN_FOCUSED).get(ctrl0), - "Ctrl+0 must be bound in WHEN_FOCUSED to beat the NetBeans global action"); - assertEquals("zoom-reset", - comp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).get(ctrl0)); - Action action = comp.getActionMap().get("zoom-reset"); - assertNotNull(action); - action.actionPerformed(null); - assertEquals(1, resets[0], "the bound action must invoke resetZoom()"); + @Test + void createClickListener_plainMiddleClickIgnored() { + int[] resets = {0}; + Zoomable z = new Zoomable() { + @Override public void zoomIn() {} + @Override public void zoomOut() {} + @Override public void resetZoom() { resets[0]++; } + @Override public int getZoomDelta() { return 0; } + @Override public int getMinDelta() { return -8; } + @Override public int getMaxDelta() { return 20; } + }; + MouseAdapter listener = ZoomSupport.createClickListener(z); + listener.mousePressed(mouseButton(new JLabel(), MouseEvent.BUTTON2, + InputEvent.getMaskForButton(MouseEvent.BUTTON2))); + assertEquals(0, resets[0]); + } + + @Test + void createClickListener_altLeftClickIgnored() { + int[] resets = {0}; + Zoomable z = new Zoomable() { + @Override public void zoomIn() {} + @Override public void zoomOut() {} + @Override public void resetZoom() { resets[0]++; } + @Override public int getZoomDelta() { return 0; } + @Override public int getMinDelta() { return -8; } + @Override public int getMaxDelta() { return 20; } + }; + MouseAdapter listener = ZoomSupport.createClickListener(z); + listener.mousePressed(mouseButton(new JLabel(), MouseEvent.BUTTON1, + InputEvent.ALT_DOWN_MASK | InputEvent.getMaskForButton(MouseEvent.BUTTON1))); + assertEquals(0, resets[0]); } }