修复设置后导航与顶部栏契约
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user