diff --git a/CLAUDE.md b/CLAUDE.md index 692f2daa..c5e2607f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,7 +145,7 @@ The plugin follows an MVC structure for session management: | `settings/` | `ClaudeCodePreferences` — NbPreferences wrapper (keys: `contextMenuSessionMode` default CONTINUE_LAST, `sessionListLimit` default 30); `ClaudeCodeOptionsPanelController` / `ClaudeCodeOptionsPanel` — Tools→Options (General + Profiles tabs); `ClaudeProfile`, `ClaudeProfileStore` — named profiles with isolated `CLAUDE_CONFIG_DIR`, auth, proxy, extra env vars; `ClaudeProjectProperties` — per-project profile assignment | | `actions/` | `ClaudeCodeAction` — toolbar button; `OpenWithClaudeAction` — project node context menu; `PromptFavoriteAction` — dynamically registered action per favorite for Keymap API | | `io.github.nbclaudecodegui.mcp` | `MCPSseServer` — Jetty `/sse` `/messages` `/hook`; `NetBeansMCPHandler` — MCP dispatcher + PreToolUse hook; `tools/` — `OpenDiff`, `PermissionPromptTool`, `DiffTabTracker`, `GetDiagnostics`, `GetOpenEditors`, `GetCurrentSelection`, `OpenFile` | -| `org.openbeans.claude.netbeans` | Legacy classes: `ClaudeCodeStatusService`, `ClaudeCodeStatusLineElement`, `EditorUtils`, `NbUtils`; `tools/` — `AsyncHandler`, `AsyncResponse`, `GetWorkspaceFolders`, `CheckDocumentDirty`, `SaveDocument`, `CloseTab`, `CloseAllDiffTabs` | +| `org.openbeans.claude.netbeans` | **Legacy package — do not add new code here and avoid modifying existing code.** Contains: `ClaudeCodeStatusService`, `ClaudeCodeStatusLineElement`, `EditorUtils`, `NbUtils`; `tools/` — `AsyncHandler`, `AsyncResponse`, `GetWorkspaceFolders`, `CheckDocumentDirty`, `SaveDocument`, `CloseTab`, `CloseAllDiffTabs` | ### Session lifecycle 1. User clicks toolbar → `ClaudeSessionTab` opens; user picks a directory and session mode in `ClaudeSessionSelectorPanel` → `ClaudeSessionController.startProcess()` merges `settings.local.json` and launches the PTY. diff --git a/docs/user-manual.md b/docs/user-manual.md index b02ba6be..2e9fee5c 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -280,7 +280,7 @@ After submitting or cancelling, the input area is restored automatically. When Claude Code is about to edit or create a file, the plugin intercepts the operation and shows a diff panel **before** the change is written to disk. -The diff panel appears either **embedded in the session tab** (replacing the input area) or in a **separate IDE tab**, depending on the setting in **Tools → Options → Claude Code → General → Open diff in a separate tab**. +The diff panel appears either **embedded in the session tab** (replacing the input area) or in a **separate IDE tab**, depending on the setting in **Tools → Options → Claude Code → General → Open file diff in a separate tab**. ![Diff panel with permission bar](screenshots/diff-panel.png) @@ -343,10 +343,14 @@ Open **Tools → Options → Claude Code** in NetBeans. | History TTL (days) | 0 | Number of days after which history entries expire. 0 = keep forever. | | Send prompt key | Ctrl+Enter | Key combination that sends the prompt from the input area. | | Insert newline key | Enter | Key combination that inserts a newline in the input area. The send and newline keys are configured independently but cannot be set to the same value. | -| Open diff in a separate tab | Off | Opens the diff panel in a new IDE tab instead of embedding it in the session tab. | -| Show markdown preview for .md files in diff | On | Shows a rendered markdown preview alongside the raw diff for `.md` files. | | Start new session when opening with Claude | Off (Continue last) | Checked → always start a **New session** when opening with the project context menu or when NetBeans restores a closed session tab on restart. Unchecked → **Continue last**. | | Session list limit | 30 | Maximum number of past sessions shown in the session selector table. | +| Session tab dock position | Editor area | Where the Claude Code session tab is docked when first opened. Choices: Editor area, Right side, Left side top, Left side bottom, Bottom dock. | +| Open file diff in a separate tab | Off | Opens the diff panel in a new IDE tab instead of embedding it in the session tab. | +| File diff tab dock position | Editor area | Where the File Diff tab is docked when opened as a separate tab. Enabled only when "Open file diff in a separate tab" is checked. | +| Show markdown preview for .md files in diff | On | Shows a rendered markdown preview alongside the raw diff for `.md` files. | +| Markdown preview dock position | Right side | Where the Markdown Preview tab is docked when first opened. Choices: Editor area, Right side, Left side top, Left side bottom, Bottom dock. | +| Choice menu focus | Grab focus | Controls whether the choice menu panel grabs keyboard focus when it appears. Choices: Grab focus, Show without grabbing focus, Hide menu. | ### Advanced tab diff --git a/pom.xml b/pom.xml index 31437b7e..b09d1b82 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.10-SNAPSHOT + 0.22.13-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/settings/ClaudeCodeOptionsPanel.java b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java index 320e5744..3f7a3939 100644 --- a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java +++ b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java @@ -49,13 +49,6 @@ public final class ClaudeCodeOptionsPanel extends JPanel { "Grab focus (default)", "Show without grabbing focus", "Hide menu" }; - private static final String[] DOCK_MODE_VALUES = { - "editor", "commonpalette", "explorer", "navigator", "output" - }; - private static final String[] DOCK_MODE_LABELS = { - "Editor area (default)", "Right side", "Left side top", "Left side bottom", "Bottom dock" - }; - private static final String[] KEY_VALUES = { ClaudeCodePreferences.ENTER, ClaudeCodePreferences.SHIFT_ENTER, @@ -86,6 +79,14 @@ public final class ClaudeCodeOptionsPanel extends JPanel { private JSpinner sessionListLimitSpinner; /** Dropdown for the dock position of the Claude Code session tab. */ private javax.swing.JComboBox dockModeCombo; + /** Dropdown for the dock position of the Markdown Preview tab. */ + private javax.swing.JComboBox mdPreviewDockModeCombo; + /** Label for the Markdown Preview dock position combo. */ + private javax.swing.JLabel mdPreviewDockModeLabel; + /** Dropdown for the dock position of the File Diff tab. */ + private javax.swing.JComboBox fileDiffDockModeCombo; + /** Label for the File Diff dock position combo. */ + private javax.swing.JLabel fileDiffDockModeLabel; /** Dropdown for the choice menu focus behavior. */ private javax.swing.JComboBox choiceMenuFocusCombo; /** Spinner for the hang-detection timeout in seconds (0 = disabled). */ @@ -204,26 +205,6 @@ private JPanel buildGeneralPanel() { newlineRadios.get(val).addActionListener(e -> syncExclusion(val, false)); } - // --- diff in session --- - diffInSessionCheck = new javax.swing.JCheckBox("Open diff in a separate tab"); - GridBagConstraints diffGbc = new GridBagConstraints(); - diffGbc.gridx = 0; diffGbc.gridy = row; - diffGbc.gridwidth = 3; - diffGbc.anchor = GridBagConstraints.WEST; - diffGbc.insets = new Insets(4, 8, 4, 8); - form.add(diffInSessionCheck, diffGbc); - row++; - - // --- md preview in diff --- - mdPreviewInDiffCheck = new javax.swing.JCheckBox("Show markdown preview for .md files in diff"); - GridBagConstraints mdGbc = new GridBagConstraints(); - mdGbc.gridx = 0; mdGbc.gridy = row; - mdGbc.gridwidth = 3; - mdGbc.anchor = GridBagConstraints.WEST; - mdGbc.insets = new Insets(4, 8, 4, 8); - form.add(mdPreviewInDiffCheck, mdGbc); - row++; - // --- context-menu session mode --- startNewSessionCheck = new javax.swing.JCheckBox("Start new session when opening with Claude"); GridBagConstraints cmGbc = new GridBagConstraints(); @@ -244,11 +225,53 @@ private JPanel buildGeneralPanel() { // --- session tab dock position --- form.add(new JLabel("Session tab dock position:"), gbc(0, row, false)); - dockModeCombo = new javax.swing.JComboBox<>(DOCK_MODE_LABELS); + dockModeCombo = new javax.swing.JComboBox<>( + DockMode.labels(ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE)); dockModeCombo.setToolTipText("Where to dock the Claude Code session tab when opening it"); form.add(dockModeCombo, gbc(1, row, false)); row++; + // --- diff in session --- + diffInSessionCheck = new javax.swing.JCheckBox("Open file diff in a separate tab"); + GridBagConstraints diffGbc = new GridBagConstraints(); + diffGbc.gridx = 0; diffGbc.gridy = row; + diffGbc.gridwidth = 3; + diffGbc.anchor = GridBagConstraints.WEST; + diffGbc.insets = new Insets(4, 8, 4, 8); + form.add(diffInSessionCheck, diffGbc); + diffInSessionCheck.addActionListener(e -> updateFileDiffDockEnabled()); + row++; + + // --- file diff dock mode (enabled only when diff is in separate tab) --- + fileDiffDockModeLabel = new javax.swing.JLabel("File diff tab dock position:"); + form.add(fileDiffDockModeLabel, gbc(0, row, false)); + fileDiffDockModeCombo = new javax.swing.JComboBox<>( + DockMode.labels(ClaudeCodePreferences.DEFAULT_FILE_DIFF_DOCK_MODE)); + fileDiffDockModeCombo.setToolTipText( + "Where to dock the File Diff tab when opening it in a separate tab"); + form.add(fileDiffDockModeCombo, gbc(1, row, false)); + row++; + + // --- md preview in diff --- + mdPreviewInDiffCheck = new javax.swing.JCheckBox("Show markdown preview for .md files in diff"); + GridBagConstraints mdGbc = new GridBagConstraints(); + mdGbc.gridx = 0; mdGbc.gridy = row; + mdGbc.gridwidth = 3; + mdGbc.anchor = GridBagConstraints.WEST; + mdGbc.insets = new Insets(4, 8, 4, 8); + form.add(mdPreviewInDiffCheck, mdGbc); + row++; + + // --- markdown preview dock mode --- + mdPreviewDockModeLabel = new javax.swing.JLabel("Markdown preview dock position:"); + form.add(mdPreviewDockModeLabel, gbc(0, row, false)); + mdPreviewDockModeCombo = new javax.swing.JComboBox<>( + DockMode.labels(ClaudeCodePreferences.DEFAULT_MARKDOWN_PREVIEW_DOCK_MODE)); + mdPreviewDockModeCombo.setToolTipText( + "Where to dock the Markdown Preview tab when first opened"); + form.add(mdPreviewDockModeCombo, gbc(1, row, false)); + row++; + // --- choice menu focus mode --- form.add(new JLabel("Choice menu focus:"), gbc(0, row, false)); choiceMenuFocusCombo = new javax.swing.JComboBox<>(CHOICE_MENU_FOCUS_LABELS); @@ -391,9 +414,12 @@ void load() { ClaudeCodePreferences.getContextMenuSessionMode() == io.github.nbclaudecodegui.model.SessionMode.NEW); sessionListLimitSpinner.setValue(ClaudeCodePreferences.getSessionListLimit()); - String dockMode = ClaudeCodePreferences.getSessionDockMode(); - int dockIdx = java.util.Arrays.asList(DOCK_MODE_VALUES).indexOf(dockMode); - dockModeCombo.setSelectedIndex(dockIdx >= 0 ? dockIdx : 0); + dockModeCombo.setSelectedIndex(ClaudeCodePreferences.getSessionDockMode().ordinal()); + mdPreviewDockModeCombo.setSelectedIndex( + ClaudeCodePreferences.getMarkdownPreviewDockMode().ordinal()); + fileDiffDockModeCombo.setSelectedIndex( + ClaudeCodePreferences.getFileDiffDockMode().ordinal()); + updateFileDiffDockEnabled(); String focusMode = ClaudeCodePreferences.getChoiceMenuFocusMode(); int focusIdx = java.util.Arrays.asList(CHOICE_MENU_FOCUS_VALUES).indexOf(focusMode); choiceMenuFocusCombo.setSelectedIndex(focusIdx >= 0 ? focusIdx : 0); @@ -431,11 +457,9 @@ void store() { ? io.github.nbclaudecodegui.model.SessionMode.NEW : io.github.nbclaudecodegui.model.SessionMode.CONTINUE_LAST); ClaudeCodePreferences.setSessionListLimit((Integer) sessionListLimitSpinner.getValue()); - int dockSel = dockModeCombo.getSelectedIndex(); - ClaudeCodePreferences.setSessionDockMode( - dockSel >= 0 && dockSel < DOCK_MODE_VALUES.length - ? DOCK_MODE_VALUES[dockSel] - : ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE); + ClaudeCodePreferences.setSessionDockMode(dockModeFromCombo(dockModeCombo)); + ClaudeCodePreferences.setMarkdownPreviewDockMode(dockModeFromCombo(mdPreviewDockModeCombo)); + ClaudeCodePreferences.setFileDiffDockMode(dockModeFromCombo(fileDiffDockModeCombo)); int focusSel = choiceMenuFocusCombo.getSelectedIndex(); ClaudeCodePreferences.setChoiceMenuFocusMode( focusSel >= 0 && focusSel < CHOICE_MENU_FOCUS_VALUES.length @@ -478,6 +502,18 @@ private static String selectedValue(Map radios) { return ClaudeCodePreferences.DEFAULT_SEND_KEY; } + private static DockMode dockModeFromCombo(javax.swing.JComboBox combo) { + int sel = combo.getSelectedIndex(); + DockMode[] vals = DockMode.values(); + return (sel >= 0 && sel < vals.length) ? vals[sel] : DockMode.EDITOR; + } + + private void updateFileDiffDockEnabled() { + boolean sep = diffInSessionCheck.isSelected(); + fileDiffDockModeLabel.setEnabled(sep); + fileDiffDockModeCombo.setEnabled(sep); + } + private static GridBagConstraints gbc(int x, int y, boolean fill) { GridBagConstraints c = new GridBagConstraints(); c.gridx = x; c.gridy = y; diff --git a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java index 314350f5..163e3f0a 100644 --- a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java +++ b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java @@ -557,17 +557,97 @@ public static void setMcpEnabled(boolean v) { /** Preference key: dock mode for the Claude Code session tab. */ public static final String KEY_SESSION_DOCK_MODE = "sessionDockMode"; - /** Default: editor area. */ - public static final String DEFAULT_SESSION_DOCK_MODE = "editor"; + /** Default dock position for the session tab: editor area. */ + public static final DockMode DEFAULT_SESSION_DOCK_MODE = DockMode.EDITOR; - public static String getSessionDockMode() { - return NbPreferences.forModule(ClaudeCodePreferences.class) - .get(KEY_SESSION_DOCK_MODE, DEFAULT_SESSION_DOCK_MODE); + /** + * Returns the configured dock position for the Claude Code session tab. + * + * @return configured {@link DockMode}, or {@link DockMode#EDITOR} as fallback + */ + public static DockMode getSessionDockMode() { + String stored = NbPreferences.forModule(ClaudeCodePreferences.class) + .get(KEY_SESSION_DOCK_MODE, DEFAULT_SESSION_DOCK_MODE.getModeName()); + return DockMode.fromModeName(stored); + } + + /** + * Persists the dock position for the Claude Code session tab. + * + * @param mode desired dock position; {@code null} falls back to {@link DockMode#EDITOR} + */ + public static void setSessionDockMode(DockMode mode) { + NbPreferences.forModule(ClaudeCodePreferences.class) + .put(KEY_SESSION_DOCK_MODE, + mode != null ? mode.getModeName() : DEFAULT_SESSION_DOCK_MODE.getModeName()); + } + + // ------------------------------------------------------------------------- + // markdownPreviewDockMode + // ------------------------------------------------------------------------- + + /** Preference key: dock mode for the Markdown Preview tab. */ + public static final String KEY_MARKDOWN_PREVIEW_DOCK_MODE = "markdownPreviewDockMode"; + /** Default dock position for the Markdown Preview tab: right side. */ + public static final DockMode DEFAULT_MARKDOWN_PREVIEW_DOCK_MODE = DockMode.RIGHT; + + /** + * Returns the configured dock position for the Markdown Preview tab. + * + * @return configured {@link DockMode}, or {@link DockMode#RIGHT} as fallback + */ + public static DockMode getMarkdownPreviewDockMode() { + String stored = NbPreferences.forModule(ClaudeCodePreferences.class) + .get(KEY_MARKDOWN_PREVIEW_DOCK_MODE, + DEFAULT_MARKDOWN_PREVIEW_DOCK_MODE.getModeName()); + return DockMode.fromModeName(stored); } - public static void setSessionDockMode(String mode) { + /** + * Persists the dock position for the Markdown Preview tab. + * + * @param mode desired dock position; {@code null} falls back to {@link DockMode#RIGHT} + */ + public static void setMarkdownPreviewDockMode(DockMode mode) { + NbPreferences.forModule(ClaudeCodePreferences.class) + .put(KEY_MARKDOWN_PREVIEW_DOCK_MODE, + mode != null ? mode.getModeName() + : DEFAULT_MARKDOWN_PREVIEW_DOCK_MODE.getModeName()); + } + + // ------------------------------------------------------------------------- + // fileDiffDockMode + // ------------------------------------------------------------------------- + + /** Preference key: dock mode for the File Diff tab (separate-tab mode only). */ + public static final String KEY_FILE_DIFF_DOCK_MODE = "fileDiffDockMode"; + /** Default dock position for the File Diff tab: editor area. */ + public static final DockMode DEFAULT_FILE_DIFF_DOCK_MODE = DockMode.EDITOR; + + /** + * Returns the configured dock position for the File Diff tab. + * + *

Only used when "Open diff in a separate tab" is enabled. + * + * @return configured {@link DockMode}, or {@link DockMode#EDITOR} as fallback + */ + public static DockMode getFileDiffDockMode() { + String stored = NbPreferences.forModule(ClaudeCodePreferences.class) + .get(KEY_FILE_DIFF_DOCK_MODE, + DEFAULT_FILE_DIFF_DOCK_MODE.getModeName()); + return DockMode.fromModeName(stored); + } + + /** + * Persists the dock position for the File Diff tab. + * + * @param mode desired dock position; {@code null} falls back to {@link DockMode#EDITOR} + */ + public static void setFileDiffDockMode(DockMode mode) { NbPreferences.forModule(ClaudeCodePreferences.class) - .put(KEY_SESSION_DOCK_MODE, mode != null ? mode : DEFAULT_SESSION_DOCK_MODE); + .put(KEY_FILE_DIFF_DOCK_MODE, + mode != null ? mode.getModeName() + : DEFAULT_FILE_DIFF_DOCK_MODE.getModeName()); } // ------------------------------------------------------------------------- diff --git a/src/main/java/io/github/nbclaudecodegui/settings/DockMode.java b/src/main/java/io/github/nbclaudecodegui/settings/DockMode.java new file mode 100644 index 00000000..556835cf --- /dev/null +++ b/src/main/java/io/github/nbclaudecodegui/settings/DockMode.java @@ -0,0 +1,73 @@ +package io.github.nbclaudecodegui.settings; + +import java.util.Arrays; + +/** + * Dock positions for plugin-managed TopComponent tabs. + * + *

Each constant maps to a NetBeans window-manager mode name and a + * human-readable label shown in the Options panel. + */ +public enum DockMode { + + /** Editor area (center). */ + EDITOR ("editor", "Editor area"), + /** Right-side dock (commonpalette mode). */ + RIGHT ("commonpalette", "Right side"), + /** Left-side top dock (explorer mode). */ + LEFT_TOP ("explorer", "Left side top"), + /** Left-side bottom dock (navigator mode). */ + LEFT_BOTTOM ("navigator", "Left side bottom"), + /** Bottom dock (output mode). */ + BOTTOM ("output", "Bottom dock"); + + private final String modeName; + private final String label; + + DockMode(String modeName, String label) { + this.modeName = modeName; + this.label = label; + } + + /** + * Returns the NetBeans window-manager mode name (e.g. {@code "editor"}, + * {@code "commonpalette"}). + * + * @return mode name string + */ + public String getModeName() { return modeName; } + + /** + * Returns the human-readable label for use in UI controls. + * + * @return display label + */ + public String getLabel() { return label; } + + /** + * Returns the constant whose {@link #getModeName()} matches {@code name}, + * or {@link #EDITOR} as fallback when no match is found. + * + * @param name mode name string to look up + * @return matching constant, or {@link #EDITOR} + */ + public static DockMode fromModeName(String name) { + for (DockMode d : values()) { + if (d.modeName.equals(name)) return d; + } + return EDITOR; + } + + /** + * Returns label strings for use in a {@link javax.swing.JComboBox}, with + * {@code " (default)"} appended to the label of {@code defaultMode}. + * + * @param defaultMode the constant that should receive the "(default)" suffix + * @return array of label strings in enum declaration order + */ + public static String[] labels(DockMode defaultMode) { + return Arrays.stream(values()) + .map(d -> d == defaultMode ? d.label + " (default)" : d.label) + .toArray(String[]::new); + } +} diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java index 5a7f76b7..a211caad 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java @@ -295,14 +295,8 @@ public ClaudeSessionTab(File dir) { *

Focuses an existing idle session if one is open; otherwise opens a new one. */ private static void dockAndOpen(ClaudeSessionTab tab) { - String modeName = ClaudeCodePreferences.getSessionDockMode(); - if (!ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE.equals(modeName)) { - org.openide.windows.Mode mode = WindowManager.getDefault().findMode(modeName); - if (mode != null) { - mode.dockInto(tab); - } - } - tab.open(); + io.github.nbclaudecodegui.ui.common.UiUtils.dockAndOpen( + tab, ClaudeCodePreferences.getSessionDockMode()); } public static void openNewOrFocus() { diff --git a/src/main/java/io/github/nbclaudecodegui/ui/FileDiffOpener.java b/src/main/java/io/github/nbclaudecodegui/ui/FileDiffOpener.java index e484a98f..e86fc267 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/FileDiffOpener.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/FileDiffOpener.java @@ -343,7 +343,8 @@ public void componentClosed() { } diffTC.add(permPanel, java.awt.BorderLayout.SOUTH); - diffTC.open(); + io.github.nbclaudecodegui.ui.common.UiUtils.dockAndOpen( + diffTC, ClaudeCodePreferences.getFileDiffDockMode()); diffTC.requestActive(); SwingUtilities.invokeLater(permPanel::requestAcceptFocus); }); diff --git a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java index 57ae04c2..fa261e99 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java @@ -1,6 +1,8 @@ package io.github.nbclaudecodegui.ui; +import io.github.nbclaudecodegui.settings.ClaudeCodePreferences; import io.github.nbclaudecodegui.ui.common.MarkdownRenderer; +import io.github.nbclaudecodegui.ui.common.UiUtils; import java.awt.BorderLayout; import java.awt.Toolkit; import java.awt.datatransfer.StringSelection; @@ -164,7 +166,7 @@ public static void openLive(String filePath, String content, FileObject fo) { tab.historyIndex = 0; OPEN_TABS.put(filePath, tab); - tab.open(); + UiUtils.dockAndOpen(tab, ClaudeCodePreferences.getMarkdownPreviewDockMode()); tab.requestActive(); } @@ -262,7 +264,7 @@ public static void open(String filePath, String before, String after) { tab.setDisplayName("Preview: " + name); tab.setLayout(new BorderLayout()); tab.add(new MarkdownDiffPanel(before, after), BorderLayout.CENTER); - tab.open(); + UiUtils.dockAndOpen(tab, ClaudeCodePreferences.getMarkdownPreviewDockMode()); tab.requestActive(); } diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/UiUtils.java b/src/main/java/io/github/nbclaudecodegui/ui/common/UiUtils.java index 4f82903d..ed1033b8 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/common/UiUtils.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/common/UiUtils.java @@ -1,8 +1,11 @@ package io.github.nbclaudecodegui.ui.common; +import io.github.nbclaudecodegui.settings.DockMode; import java.awt.Color; import javax.swing.JButton; import javax.swing.UIManager; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; /** Shared UI utility helpers for the plugin. */ public final class UiUtils { @@ -15,6 +18,28 @@ public final class UiUtils { private UiUtils() {} + /** + * Docks {@code tc} into the window-manager mode specified by {@code mode}, + * then opens the component. + * + *

When {@code mode} is {@link DockMode#EDITOR} no docking call is made and + * the component opens in its default position, preserving any manual + * repositioning the user may have done. + * + * @param tc the TopComponent to open + * @param mode desired dock position + */ + public static void dockAndOpen(TopComponent tc, DockMode mode) { + if (mode != DockMode.EDITOR) { + org.openide.windows.Mode m = + WindowManager.getDefault().findMode(mode.getModeName()); + if (m != null) { + m.dockInto(tc); + } + } + tc.open(); + } + /** * 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 diff --git a/src/test/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferencesTest.java b/src/test/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferencesTest.java index 1a2a4d92..a520ea71 100644 --- a/src/test/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferencesTest.java +++ b/src/test/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferencesTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertFalse; +import io.github.nbclaudecodegui.settings.DockMode; /** * Unit tests for {@link ClaudeCodePreferences}. @@ -18,6 +19,8 @@ void tearDown() { ClaudeCodePreferences.setOpenDiffInSeparateTab(ClaudeCodePreferences.DEFAULT_OPEN_DIFF_IN_SEPARATE_TAB); ClaudeCodePreferences.setMdPreviewInDiff(ClaudeCodePreferences.DEFAULT_MD_PREVIEW_IN_DIFF); ClaudeCodePreferences.setSessionDockMode(ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE); + ClaudeCodePreferences.setMarkdownPreviewDockMode(ClaudeCodePreferences.DEFAULT_MARKDOWN_PREVIEW_DOCK_MODE); + ClaudeCodePreferences.setFileDiffDockMode(ClaudeCodePreferences.DEFAULT_FILE_DIFF_DOCK_MODE); } @Test @@ -101,21 +104,55 @@ void mdPreviewInDiffStoreAndRetrieve() { @Test void sessionDockModeDefaultIsEditor() { - assertEquals(ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE, - ClaudeCodePreferences.getSessionDockMode()); - assertEquals("editor", ClaudeCodePreferences.getSessionDockMode()); + assertEquals(DockMode.EDITOR, ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE); + assertEquals(DockMode.EDITOR, ClaudeCodePreferences.getSessionDockMode()); } @Test void sessionDockModeStoreAndRetrieve() { - ClaudeCodePreferences.setSessionDockMode("output"); - assertEquals("output", ClaudeCodePreferences.getSessionDockMode()); + ClaudeCodePreferences.setSessionDockMode(DockMode.BOTTOM); + assertEquals(DockMode.BOTTOM, ClaudeCodePreferences.getSessionDockMode()); } @Test void sessionDockModeNullFallsBackToDefault() { ClaudeCodePreferences.setSessionDockMode(null); - assertEquals(ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE, - ClaudeCodePreferences.getSessionDockMode()); + assertEquals(DockMode.EDITOR, ClaudeCodePreferences.getSessionDockMode()); + } + + @Test + void markdownPreviewDockModeDefaultIsRight() { + assertEquals(DockMode.RIGHT, ClaudeCodePreferences.DEFAULT_MARKDOWN_PREVIEW_DOCK_MODE); + assertEquals(DockMode.RIGHT, ClaudeCodePreferences.getMarkdownPreviewDockMode()); + } + + @Test + void markdownPreviewDockModeStoreAndRetrieve() { + ClaudeCodePreferences.setMarkdownPreviewDockMode(DockMode.BOTTOM); + assertEquals(DockMode.BOTTOM, ClaudeCodePreferences.getMarkdownPreviewDockMode()); + } + + @Test + void markdownPreviewDockModeNullFallsBackToDefault() { + ClaudeCodePreferences.setMarkdownPreviewDockMode(null); + assertEquals(DockMode.RIGHT, ClaudeCodePreferences.getMarkdownPreviewDockMode()); + } + + @Test + void fileDiffDockModeDefaultIsEditor() { + assertEquals(DockMode.EDITOR, ClaudeCodePreferences.DEFAULT_FILE_DIFF_DOCK_MODE); + assertEquals(DockMode.EDITOR, ClaudeCodePreferences.getFileDiffDockMode()); + } + + @Test + void fileDiffDockModeStoreAndRetrieve() { + ClaudeCodePreferences.setFileDiffDockMode(DockMode.BOTTOM); + assertEquals(DockMode.BOTTOM, ClaudeCodePreferences.getFileDiffDockMode()); + } + + @Test + void fileDiffDockModeNullFallsBackToDefault() { + ClaudeCodePreferences.setFileDiffDockMode(null); + assertEquals(DockMode.EDITOR, ClaudeCodePreferences.getFileDiffDockMode()); } } diff --git a/src/test/java/io/github/nbclaudecodegui/settings/DockModeTest.java b/src/test/java/io/github/nbclaudecodegui/settings/DockModeTest.java new file mode 100644 index 00000000..ccf1ba28 --- /dev/null +++ b/src/test/java/io/github/nbclaudecodegui/settings/DockModeTest.java @@ -0,0 +1,56 @@ +package io.github.nbclaudecodegui.settings; + +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link DockMode}. + */ +class DockModeTest { + + @Test + void fromModeNameKnownValue() { + assertEquals(DockMode.RIGHT, DockMode.fromModeName("commonpalette")); + } + + @Test + void fromModeNameUnknownFallsBackToEditor() { + assertEquals(DockMode.EDITOR, DockMode.fromModeName("unknown")); + } + + @Test + void fromModeNameNullFallsBackToEditor() { + assertEquals(DockMode.EDITOR, DockMode.fromModeName(null)); + } + + @Test + void labelsLengthMatchesEnumSize() { + assertEquals(DockMode.values().length, DockMode.labels(DockMode.EDITOR).length); + } + + @Test + void labelsContainsDefaultSuffix() { + assertTrue(Arrays.stream(DockMode.labels(DockMode.RIGHT)) + .anyMatch(l -> l.contains("(default)"))); + } + + @Test + void labelsDefaultSuffixOnCorrectEntry() { + String[] labels = DockMode.labels(DockMode.RIGHT); + int rightOrdinal = DockMode.RIGHT.ordinal(); + assertTrue(labels[rightOrdinal].contains("(default)")); + } + + @Test + void labelsNoDefaultSuffixOnOtherEntries() { + String[] labels = DockMode.labels(DockMode.RIGHT); + int rightOrdinal = DockMode.RIGHT.ordinal(); + for (int i = 0; i < labels.length; i++) { + if (i != rightOrdinal) { + assertFalse(labels[i].contains("(default)"), + "Entry " + i + " should not have (default) suffix"); + } + } + } +}