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"); + } + } + } +}