Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.23.18-SNAPSHOT</version>
<version>0.23.19-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Plugin Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
15 changes: 9 additions & 6 deletions src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}

Expand Down
46 changes: 35 additions & 11 deletions src/main/java/io/github/nbclaudecodegui/ui/common/ZoomSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(); }
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {}
Expand All @@ -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]);
}
}
Loading