From 7044d5ec145113cc0af3d6f22220cf0efc268083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Sat, 1 Aug 2026 15:16:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=BE=E7=BD=AE=E5=90=8E?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E4=B8=8E=E9=A1=B6=E9=83=A8=E6=A0=8F=E5=A5=91?= =?UTF-8?q?=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workutils/system/ui/MainWindow.java | 52 +++++------ .../system/ui/shell/NavigationRail.java | 8 ++ .../system/ui/MainWindowArchitectureTest.java | 14 +++ .../system/ui/shell/NavigationRailTest.java | 88 ++++++++++++++++--- 4 files changed, 122 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/example/workutils/system/ui/MainWindow.java b/src/main/java/com/example/workutils/system/ui/MainWindow.java index 29c6469..a78247e 100644 --- a/src/main/java/com/example/workutils/system/ui/MainWindow.java +++ b/src/main/java/com/example/workutils/system/ui/MainWindow.java @@ -43,8 +43,7 @@ public class MainWindow { private String activePluginId; private EmbeddedEntry currentEmbedded; - private HBox pluginToolbar; - private FontIcon toolbarIcon; + private FontIcon toolbarChevron; private Label toolbarTitle; public MainWindow(List navItems, WorkUtilsProperties properties, Runnable openSettings) { @@ -54,12 +53,6 @@ public class MainWindow { buildUI(Objects.requireNonNull(openSettings, "openSettings must not be null")); } - /** Compatibility constructor for callers that do not expose settings. */ - public MainWindow(List navItems, WorkUtilsProperties properties) { - this(navItems, properties, () -> { - }); - } - public void refreshNavItems(List newItems) { navItems = new ArrayList<>(newItems); navigationRail.setItems(navItems); @@ -129,17 +122,14 @@ public class MainWindow { brand.setContentDisplay(ContentDisplay.LEFT); brand.setFont(Theme.sectionTitle()); - pluginToolbar = new HBox(Theme.S2); - pluginToolbar.setAlignment(Pos.CENTER_LEFT); - pluginToolbar.setVisible(false); - pluginToolbar.setManaged(false); - - FontIcon chevron = IconFactory.create(AppIcon.CHEVRON_RIGHT, 14); - toolbarIcon = IconFactory.create(AppIcon.APP, 18); + toolbarChevron = IconFactory.create(AppIcon.CHEVRON_RIGHT, 14); + toolbarChevron.setVisible(false); + toolbarChevron.setManaged(false); toolbarTitle = new Label(); toolbarTitle.setFont(Theme.pageTitle()); toolbarTitle.getStyleClass().add(Theme.CSS_PAGE_TITLE); - pluginToolbar.getChildren().addAll(chevron, toolbarIcon, toolbarTitle); + toolbarTitle.setVisible(false); + toolbarTitle.setManaged(false); Region spacerRight = new Region(); HBox.setHgrow(spacerRight, Priority.ALWAYS); @@ -151,7 +141,7 @@ public class MainWindow { settingsButton.setOnAction(event -> openSettings.run()); bar.getChildren().addAll( - menuButton, brand, pluginToolbar, spacerRight, settingsButton); + menuButton, brand, toolbarChevron, toolbarTitle, spacerRight, settingsButton); return bar; } @@ -163,16 +153,19 @@ public class MainWindow { showEmbeddedFor(item); } - private void showPluginToolbar(AppIcon icon, String title) { - toolbarIcon.setIconCode(icon.glyph()); + private void showPluginToolbar(String title) { toolbarTitle.setText(title); - pluginToolbar.setVisible(true); - pluginToolbar.setManaged(true); + toolbarChevron.setVisible(true); + toolbarChevron.setManaged(true); + toolbarTitle.setVisible(true); + toolbarTitle.setManaged(true); } private void hidePluginToolbar() { - pluginToolbar.setVisible(false); - pluginToolbar.setManaged(false); + toolbarChevron.setVisible(false); + toolbarChevron.setManaged(false); + toolbarTitle.setVisible(false); + toolbarTitle.setManaged(false); } private void showWelcomePage() { @@ -215,12 +208,15 @@ public class MainWindow { public void embedPlugin(String pluginId, String ignoredIcon, String name, Parent content) { embeddedCache.remove(pluginId); - EmbeddedEntry entry = new EmbeddedEntry(content, NavItem.iconForPlugin(pluginId), name); + EmbeddedEntry entry = new EmbeddedEntry(content, name); embeddedCache.put(pluginId, entry); switchToEmbedded(pluginId, entry); } private void switchToEmbedded(String pluginId, EmbeddedEntry entry) { + if ("__settings__".equals(pluginId)) { + navigationRail.clearSelection(); + } if (currentEmbedded != null && currentEmbedded != entry) { contentArea.getChildren().remove(currentEmbedded.wrapper); } @@ -235,7 +231,7 @@ public class MainWindow { if (!contentArea.getChildren().contains(entry.wrapper)) { contentArea.getChildren().setAll(entry.wrapper); } - showPluginToolbar(entry.icon, entry.title); + showPluginToolbar(entry.title); } public void closePlugin(String pluginId) { @@ -264,7 +260,7 @@ public class MainWindow { wrapper.getStyleClass().add(Theme.CSS_CONTENT_AREA); wrapper.setCenter(content); contentArea.getChildren().setAll(wrapper); - showPluginToolbar(AppIcon.APP, title); + showPluginToolbar(title); } public void refreshTheme() { @@ -273,13 +269,11 @@ public class MainWindow { private static final class EmbeddedEntry { private final Parent content; - private final AppIcon icon; private final String title; private BorderPane wrapper; - private EmbeddedEntry(Parent content, AppIcon icon, String title) { + private EmbeddedEntry(Parent content, String title) { this.content = content; - this.icon = icon; this.title = title; } } diff --git a/src/main/java/com/example/workutils/system/ui/shell/NavigationRail.java b/src/main/java/com/example/workutils/system/ui/shell/NavigationRail.java index 764bcba..2d3dd39 100644 --- a/src/main/java/com/example/workutils/system/ui/shell/NavigationRail.java +++ b/src/main/java/com/example/workutils/system/ui/shell/NavigationRail.java @@ -96,6 +96,14 @@ public final class NavigationRail extends VBox { return selectedPluginId; } + public void clearSelection() { + requireFxThread(); + selection.selectToggle(null); + itemViews.forEach(view -> view.button().getStyleClass().remove(Theme.CSS_NAV_CELL_ACTIVE)); + selectedItem = null; + selectedPluginId = null; + } + public void setItems(List items) { requireFxThread(); Objects.requireNonNull(items, "items must not be null"); diff --git a/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java b/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java index ffc6e7d..f32a85e 100644 --- a/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java +++ b/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java @@ -2,8 +2,10 @@ package com.example.workutils.system.ui; import org.junit.jupiter.api.Test; +import java.lang.reflect.Constructor; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -11,6 +13,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class MainWindowArchitectureTest { + @Test + void mainWindowRequiresAnExplicitSettingsAction() { + Constructor[] constructors = MainWindow.class.getConstructors(); + + assertEquals(1, constructors.length); + assertEquals(List.class, constructors[0].getParameterTypes()[0]); + assertEquals(com.example.workutils.system.config.WorkUtilsProperties.class, + constructors[0].getParameterTypes()[1]); + assertEquals(Runnable.class, constructors[0].getParameterTypes()[2]); + } + @Test void settingsIsAUniqueHeaderActionInsteadOfANavigationItem() throws Exception { String application = source("src/main/java/com/example/workutils/WorkUtilsApplication.java"); @@ -43,6 +56,7 @@ class MainWindowArchitectureTest { assertTrue(header.contains("IconFactory.createButtonIcon(AppIcon.SETTINGS")); assertEquals(1, occurrences(header, "AppIcon.SETTINGS")); assertEquals(1, occurrences(header, "openSettings.run()")); + assertFalse(header.contains("toolbarIcon")); assertFalse(header.contains("SVGPath")); assertFalse(header.contains(" { - MainWindow window = new MainWindow(List.of(), new WorkUtilsProperties()); - + MainWindow window = new MainWindow(List.of(), new WorkUtilsProperties(), () -> { + }); window.embedPlugin("api-doc", "legacy-api-icon", "API", new StackPane()); - assertEquals(AppIcon.API_DOC.glyph(), toolbarIcon(window).getIconCode()); + HBox header = assertInstanceOf( + HBox.class, stage(window).getScene().getRoot().lookup("#app-header")); - window.embedPlugin("mobile-ui-auto", "legacy-mobile-icon", "Mobile", new StackPane()); - assertEquals(AppIcon.APP.glyph(), toolbarIcon(window).getIconCode()); + assertEquals(6, header.getChildren().size()); + Button menu = assertInstanceOf(Button.class, header.getChildren().get(0)); + Label brand = assertInstanceOf(Label.class, header.getChildren().get(1)); + FontIcon chevron = assertInstanceOf(FontIcon.class, header.getChildren().get(2)); + Label title = assertInstanceOf(Label.class, header.getChildren().get(3)); + assertInstanceOf(Region.class, header.getChildren().get(4)); + Button settings = assertInstanceOf(Button.class, header.getChildren().get(5)); - window.embedPlugin("third-party", "legacy-third-party-icon", "Other", new StackPane()); - assertEquals(AppIcon.APP.glyph(), toolbarIcon(window).getIconCode()); + assertEquals(AppIcon.MENU.glyph(), + assertInstanceOf(FontIcon.class, menu.getGraphic()).getIconCode()); + assertEquals(AppIcon.APP.glyph(), + assertInstanceOf(FontIcon.class, brand.getGraphic()).getIconCode()); + assertEquals(AppIcon.CHEVRON_RIGHT.glyph(), chevron.getIconCode()); + assertEquals("API", title.getText()); + assertEquals(AppIcon.SETTINGS.glyph(), + assertInstanceOf(FontIcon.class, settings.getGraphic()).getIconCode()); + }); + } + + @Test + void settingsRunsOnceUpdatesTitleAndAllowsOneClickReturnToSelectedPlugin() throws Exception { + fx(() -> { + AtomicReference windowRef = new AtomicReference<>(); + AtomicInteger pluginOpens = new AtomicInteger(); + AtomicInteger settingsOpens = new AtomicInteger(); + MainWindow.NavItem plugin = new MainWindow.NavItem( + AppIcon.API_DOC, "API", "API plugin", () -> { + pluginOpens.incrementAndGet(); + windowRef.get().embedPlugin( + "api-doc", "legacy-api-icon", "API", new StackPane()); + }, "api-doc"); + MainWindow window = new MainWindow(List.of(plugin), new WorkUtilsProperties(), () -> { + settingsOpens.incrementAndGet(); + windowRef.get().embedPlugin( + "__settings__", "legacy-settings-icon", "系统设置", new StackPane()); + }); + windowRef.set(window); + Parent root = stage(window).getScene().getRoot(); + ToggleButton pluginButton = assertInstanceOf( + ToggleButton.class, root.lookup("#nav-api-doc")); + Button settingsButton = assertInstanceOf( + Button.class, root.lookup("#settings-button")); + NavigationRail rail = assertInstanceOf( + NavigationRail.class, root.lookup(".navigation-rail")); + + pluginButton.fire(); + settingsButton.fire(); + + assertEquals(1, settingsOpens.get()); + assertEquals(1, pluginOpens.get()); + assertEquals("系统设置", toolbarTitle(window).getText()); + assertNull(rail.selectedPluginId()); + assertFalse(pluginButton.isSelected()); + + pluginButton.fire(); + + assertEquals(2, pluginOpens.get()); + assertEquals("API", toolbarTitle(window).getText()); }); } @@ -198,9 +258,15 @@ class NavigationRailTest { }); } - private static FontIcon toolbarIcon(MainWindow window) throws ReflectiveOperationException { - Field field = MainWindow.class.getDeclaredField("toolbarIcon"); + private static Label toolbarTitle(MainWindow window) throws ReflectiveOperationException { + Field field = MainWindow.class.getDeclaredField("toolbarTitle"); field.setAccessible(true); - return assertInstanceOf(FontIcon.class, field.get(window)); + return assertInstanceOf(Label.class, field.get(window)); + } + + private static Stage stage(MainWindow window) throws ReflectiveOperationException { + Field field = MainWindow.class.getDeclaredField("stage"); + field.setAccessible(true); + return assertInstanceOf(Stage.class, field.get(window)); } }