diff --git a/CHANGELOG.md b/CHANGELOG.md index 9568ed36..f3fbce39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed Accept/Decline and Yes/No button colors to respect the IDE Look and Feel theme (dark themes no longer show clashing hardcoded colors) - Added session management when opening a session from the toolbar and when closing a tab via the Close button - Added configurable session mode: choose to resume the last session or start a new one diff --git a/pom.xml b/pom.xml index ad81952d..c6e262f8 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.21.12-SNAPSHOT + 0.21.14-SNAPSHOT nbm Netbeans 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/ChoiceMenuPanel.java b/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java index 39e18515..e381d929 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java @@ -2,6 +2,7 @@ import io.github.nbclaudecodegui.model.ChoiceMenuModel; import io.github.nbclaudecodegui.ui.common.DecoratedTextField; +import io.github.nbclaudecodegui.ui.common.UiUtils; import java.awt.Color; import java.awt.Component; import java.awt.event.KeyEvent; @@ -117,23 +118,10 @@ public void show(ChoiceMenuModel model, Consumer callback) { String display = opt.display().trim(); String response = opt.response(); String label = display.equalsIgnoreCase("Yes") - ? FileDiffPermissionPanel.ICON_ACCEPT + " " + display - : FileDiffPermissionPanel.ICON_DECLINE + " " + display; + ? UiUtils.ICON_CHECK + " " + display + : UiUtils.ICON_CROSS + " " + display; JButton btn = new JButton(label); - Color baseColor = display.equalsIgnoreCase("Yes") - ? new Color(34, 139, 34) : new Color(178, 34, 34); - Color focusColor = baseColor.brighter(); - btn.setBackground(baseColor); - btn.setForeground(Color.WHITE); - btn.setOpaque(true); - btn.addFocusListener(new java.awt.event.FocusAdapter() { - @Override public void focusGained(java.awt.event.FocusEvent e) { - btn.setBackground(focusColor); - } - @Override public void focusLost(java.awt.event.FocusEvent e) { - btn.setBackground(baseColor); - } - }); + UiUtils.applyActionStyle(btn, display.equalsIgnoreCase("Yes")); btn.addActionListener(e -> { LOG.info("[ChoiceMenuPanel] yes/no clicked: \"" + display + "\" → \"" + response + "\""); submitAnswer(response); diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ClaudePromptPanel.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudePromptPanel.java index e470f67d..cbbc76bb 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudePromptPanel.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudePromptPanel.java @@ -3,6 +3,7 @@ import io.github.nbclaudecodegui.settings.ClaudeCodePreferences; import io.github.nbclaudecodegui.ui.common.AtPathHighlighter; import io.github.nbclaudecodegui.ui.common.DecoratedTextArea; +import io.github.nbclaudecodegui.ui.common.UiUtils; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; @@ -134,8 +135,8 @@ public ClaudePromptPanel(Consumer onSend, // Install @-path highlighter (DecoratedTextArea implements RangeHighlightable) pathHighlighter = AtPathHighlighter.install(inputArea); - sendButton = new JButton("" + ICON_SEND + " Send"); - cancelButton = new JButton("" + ICON_CANCEL + " Cancel"); + sendButton = new JButton("" + ICON_SEND + " Send"); + cancelButton = new JButton("" + ICON_CANCEL + " Cancel"); sendButton.setEnabled(false); cancelButton.setEnabled(false); sendButton.addActionListener(e -> doSend()); @@ -151,7 +152,7 @@ public ClaudePromptPanel(Consumer onSend, buttonCol.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); JButton historyButton = new JButton("" + ICON_HISTORY + " History"); - JButton favoritesButton = new JButton("" + ICON_FAVORITES + " Favorites"); + JButton favoritesButton = new JButton("" + ICON_FAVORITES + " Favorites"); historyButton.setAlignmentX(Component.CENTER_ALIGNMENT); favoritesButton.setAlignmentX(Component.CENTER_ALIGNMENT); historyButton.setToolTipText("Browse prompt history"); diff --git a/src/main/java/io/github/nbclaudecodegui/ui/FileDiffPermissionPanel.java b/src/main/java/io/github/nbclaudecodegui/ui/FileDiffPermissionPanel.java index 8c0b8075..d2a0ac27 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/FileDiffPermissionPanel.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/FileDiffPermissionPanel.java @@ -1,6 +1,7 @@ package io.github.nbclaudecodegui.ui; import io.github.nbclaudecodegui.ui.common.DecoratedTextField; +import io.github.nbclaudecodegui.ui.common.UiUtils; import java.awt.BorderLayout; import java.awt.Color; import java.awt.FlowLayout; @@ -40,14 +41,8 @@ public final class FileDiffPermissionPanel extends JPanel { /** Placeholder text shown in the decline reason field when it is empty. */ static final String REASON_HINT = "Decline reason (Optional)"; - /** \u2713 CHECK MARK — used on confirmation buttons (Accept, Yes) */ - public static final String ICON_ACCEPT = "\u2713"; - /** Double check mark — used on AcceptAll button */ - private static final String ICON_ACCEPT_ALL = ICON_ACCEPT + ICON_ACCEPT; - - /** \u2717 BALLOT X — used on decline buttons (Decline, No) */ - public static final String ICON_DECLINE = "\u2717"; + private static final String ICON_ACCEPT_ALL = UiUtils.ICON_CHECK + UiUtils.ICON_CHECK; /** * Creates the panel. @@ -72,34 +67,14 @@ public FileDiffPermissionPanel(Runnable onAccept, Consumer onDecline, acceptAllBtn.setEnabled(onAcceptAll != null); // Accept button (green) - JButton acceptBtn = new JButton(ICON_ACCEPT + " Accept"); + JButton acceptBtn = new JButton(UiUtils.ICON_CHECK + " Accept"); acceptBtn.setToolTipText("Accept this change"); - acceptBtn.setOpaque(true); - acceptBtn.setBackground(new Color(34, 139, 34)); - acceptBtn.setForeground(Color.WHITE); - acceptBtn.addFocusListener(new java.awt.event.FocusAdapter() { - @Override public void focusGained(java.awt.event.FocusEvent e) { - acceptBtn.setBackground(new Color(34, 139, 34).brighter()); - } - @Override public void focusLost(java.awt.event.FocusEvent e) { - acceptBtn.setBackground(new Color(34, 139, 34)); - } - }); + UiUtils.applyActionStyle(acceptBtn, true); // Decline button (red) - JButton declineBtn = new JButton(ICON_DECLINE + " Decline"); + JButton declineBtn = new JButton(UiUtils.ICON_CROSS + " Decline"); declineBtn.setToolTipText("Decline this change"); - declineBtn.setOpaque(true); - declineBtn.setBackground(new Color(178, 34, 34)); - declineBtn.setForeground(Color.WHITE); - declineBtn.addFocusListener(new java.awt.event.FocusAdapter() { - @Override public void focusGained(java.awt.event.FocusEvent e) { - declineBtn.setBackground(new Color(178, 34, 34).brighter()); - } - @Override public void focusLost(java.awt.event.FocusEvent e) { - declineBtn.setBackground(new Color(178, 34, 34)); - } - }); + UiUtils.applyActionStyle(declineBtn, false); // Reason field DecoratedTextField reasonField = new DecoratedTextField(() -> wdFinal); diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/UiUtils.java b/src/main/java/io/github/nbclaudecodegui/ui/common/UiUtils.java new file mode 100644 index 00000000..4f82903d --- /dev/null +++ b/src/main/java/io/github/nbclaudecodegui/ui/common/UiUtils.java @@ -0,0 +1,79 @@ +package io.github.nbclaudecodegui.ui.common; + +import java.awt.Color; +import javax.swing.JButton; +import javax.swing.UIManager; + +/** Shared UI utility helpers for the plugin. */ +public final class UiUtils { + + /** ✓ CHECK MARK — used on confirmation/positive action buttons. */ + public static final String ICON_CHECK = "\u2713"; + + /** ✗ BALLOT X — used on decline/negative action buttons. */ + public static final String ICON_CROSS = "\u2717"; + + private UiUtils() {} + + /** + * Resolves a theme-aware color: tries the FlatLaf {@code UIManager} key first; + * falls back to {@code light} or {@code dark} based on the luminance of + * {@code Panel.background} (sum of RGB components < 384 → dark theme). + * + * @param flatLafKey UIManager key to try first (e.g. {@code "Actions.Green"}) + * @param light color to use in light themes when the key is absent + * @param dark color to use in dark themes when the key is absent + * @return resolved {@link Color}, never {@code null} + */ + public static Color resolveColor(String flatLafKey, Color light, Color dark) { + Color resolved = UIManager.getColor(flatLafKey); + if (resolved != null) return resolved; + Color panelBg = UIManager.getColor("Panel.background"); + boolean isDark = panelBg != null + && (panelBg.getRed() + panelBg.getGreen() + panelBg.getBlue()) < 384; + return isDark ? dark : light; + } + + /** Returns the theme-aware positive/confirm color (green). */ + public static Color getPositiveColor() { + return resolveColor("Actions.Green", new Color(34, 139, 34), new Color(60, 160, 60)); + } + + /** Returns the theme-aware negative/cancel color (red). */ + public static Color getNegativeColor() { + return resolveColor("Actions.Red", new Color(178, 34, 34), new Color(180, 60, 60)); + } + + /** Returns the theme-aware warning/accent color (orange). */ + public static Color getWarningColor() { + return resolveColor("Actions.Yellow", new Color(180, 100, 0), new Color(200, 130, 0)); + } + + /** + * Formats a {@link Color} as an HTML hex string (e.g. {@code "#228B22"}). + * Useful for {@code } in HTML-rendered Swing labels/buttons. + * + * @param c the color to format + * @return six-digit uppercase hex string prefixed with {@code #} + */ + public static String toHex(Color c) { + return String.format("#%02X%02X%02X", c.getRed(), c.getGreen(), c.getBlue()); + } + + /** + * Applies a theme-aware green ({@code positive=true}) or red ({@code positive=false}) + * background style to a button: sets opaque background, white foreground, and a + * brighter focus highlight. Uses {@link #getPositiveColor()} / {@link #getNegativeColor()}. + */ + public static void applyActionStyle(JButton btn, boolean positive) { + final Color base = positive ? getPositiveColor() : getNegativeColor(); + final Color focus = base.brighter(); + btn.setOpaque(true); + btn.setBackground(base); + btn.setForeground(Color.WHITE); + btn.addFocusListener(new java.awt.event.FocusAdapter() { + @Override public void focusGained(java.awt.event.FocusEvent e) { btn.setBackground(focus); } + @Override public void focusLost(java.awt.event.FocusEvent e) { btn.setBackground(base); } + }); + } +} diff --git a/src/test/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanelTest.java b/src/test/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanelTest.java index 949d4db8..a1804494 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanelTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanelTest.java @@ -2,6 +2,7 @@ import io.github.nbclaudecodegui.model.ChoiceMenuModel; import io.github.nbclaudecodegui.model.ChoiceMenuModel.Option; +import io.github.nbclaudecodegui.ui.common.UiUtils; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; @@ -44,8 +45,8 @@ void testShowWithOptionsCreatesButtons() throws Exception { assertTrue(panel.getComponentCount() > 0, "panel should have components after show()"); List btnLabels = collectButtonLabels(panel); - String yesLabel = FileDiffPermissionPanel.ICON_ACCEPT + " Yes"; - String noLabel = FileDiffPermissionPanel.ICON_DECLINE + " No"; + String yesLabel = UiUtils.ICON_CHECK + " Yes"; + String noLabel = UiUtils.ICON_CROSS + " No"; assertTrue(btnLabels.contains(yesLabel), "should have '" + yesLabel + "' button"); assertTrue(btnLabels.contains(noLabel), "should have '" + noLabel + "' button"); }); @@ -59,7 +60,7 @@ void testYesButtonIsGreen() throws Exception { List.of(new Option("Yes", "1"), new Option("No", "2")), -1); panel.show(model, answer -> {}); - JButton btn = findButton(panel, FileDiffPermissionPanel.ICON_ACCEPT + " Yes"); + JButton btn = findButton(panel, UiUtils.ICON_CHECK + " Yes"); assertNotNull(btn, "Yes button must exist"); assertEquals(new Color(34, 139, 34), btn.getBackground(), "Yes button should be green"); }); @@ -73,7 +74,7 @@ void testNoButtonIsRed() throws Exception { List.of(new Option("Yes", "1"), new Option("No", "2")), -1); panel.show(model, answer -> {}); - JButton btn = findButton(panel, FileDiffPermissionPanel.ICON_DECLINE + " No"); + JButton btn = findButton(panel, UiUtils.ICON_CROSS + " No"); assertNotNull(btn, "No button must exist"); assertEquals(new Color(178, 34, 34), btn.getBackground(), "No button should be red"); }); @@ -183,7 +184,7 @@ void testButtonClickInvokesCallbackWithResponse() throws Exception { List.of(new Option("Yes", "1"), new Option("No", "2")), -1); panel.show(model, received::add); - JButton yesBtn = findButton(panel, FileDiffPermissionPanel.ICON_ACCEPT + " Yes"); + JButton yesBtn = findButton(panel, UiUtils.ICON_CHECK + " Yes"); assertNotNull(yesBtn, "Yes button must exist"); yesBtn.doClick(); }); @@ -199,7 +200,7 @@ void testHideAfterButtonClick() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Go?", List.of(new Option("Yes", "1")), -1); panel.show(model, answer -> {}); - JButton btn = findButton(panel, FileDiffPermissionPanel.ICON_ACCEPT + " Yes"); + JButton btn = findButton(panel, UiUtils.ICON_CHECK + " Yes"); assertNotNull(btn); btn.doClick();