修复设置后导航与顶部栏契约

This commit is contained in:
张家训
2026-08-01 15:16:10 +08:00
parent 145b6559b8
commit 7044d5ec14
4 changed files with 122 additions and 40 deletions
@@ -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<NavItem> 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<NavItem> navItems, WorkUtilsProperties properties) {
this(navItems, properties, () -> {
});
}
public void refreshNavItems(List<NavItem> 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;
}
}
@@ -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<MainWindow.NavItem> items) {
requireFxThread();
Objects.requireNonNull(items, "items must not be null");
@@ -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("<svg"));
assertFalse(mainWindow.contains("StageStyle.UNDECORATED"));
@@ -6,9 +6,15 @@ import com.example.workutils.system.ui.Theme;
import com.example.workutils.system.ui.icon.AppIcon;
import com.example.workutils.system.config.WorkUtilsProperties;
import javafx.css.PseudoClass;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.junit.jupiter.api.Test;
import org.kordamp.ikonli.javafx.FontIcon;
@@ -38,18 +44,72 @@ class NavigationRailTest {
"api-doc");
@Test
void pluginToolbarUsesFeatherSemanticsInsteadOfLegacyTextIcons() throws Exception {
void headerHasOnlyMenuBrandChevronTitleSpacerAndSettings() throws Exception {
fx(() -> {
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<MainWindow> 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));
}
}