统一应用顶部栏与设置入口
This commit is contained in:
@@ -111,9 +111,9 @@ public class WorkUtilsApplication implements CommandLineRunner {
|
||||
Platform.runLater(Theme::initTheme);
|
||||
|
||||
// 设置导航栏刷新回调(插件管理中启用/禁用后刷新侧边栏)
|
||||
settingsService.setNavItemsRefresher(items -> Platform.runLater(() -> {
|
||||
settingsService.setNavItemsRefresher(ignoredItems -> Platform.runLater(() -> {
|
||||
if (mainWindow != null) {
|
||||
mainWindow.refreshNavItems(items);
|
||||
mainWindow.refreshNavItems(buildNavItems());
|
||||
// 重新绑定插件面板
|
||||
for (Plugin plugin : pluginManager.getEnabledPlugins()) {
|
||||
PluginDescriptor desc = plugin.getDescriptor();
|
||||
@@ -131,7 +131,7 @@ public class WorkUtilsApplication implements CommandLineRunner {
|
||||
CountDownLatch windowClosed = new CountDownLatch(1);
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
mainWindow = new MainWindow(navItems, properties);
|
||||
mainWindow = new MainWindow(navItems, properties, this::openSettingsEmbedded);
|
||||
mainWindow.showAndWait();
|
||||
} catch (Exception e) {
|
||||
log.error("主窗口创建失败", e);
|
||||
@@ -164,14 +164,6 @@ public class WorkUtilsApplication implements CommandLineRunner {
|
||||
));
|
||||
}
|
||||
|
||||
// 系统设置 — 内嵌为 Tab
|
||||
items.add(new MainWindow.NavItem(
|
||||
"⚙️", "系统设置",
|
||||
"配置系统路径、数据库连接、项目路径、多数据源、字体大小等",
|
||||
this::openSettingsEmbedded,
|
||||
"__settings__"
|
||||
));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -25,6 +28,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Main application window and shell-level plugin content host. */
|
||||
public class MainWindow {
|
||||
@@ -43,11 +47,17 @@ public class MainWindow {
|
||||
private FontIcon toolbarIcon;
|
||||
private Label toolbarTitle;
|
||||
|
||||
public MainWindow(List<NavItem> navItems, WorkUtilsProperties properties) {
|
||||
public MainWindow(List<NavItem> navItems, WorkUtilsProperties properties, Runnable openSettings) {
|
||||
this.navItems = new ArrayList<>(navItems);
|
||||
this.properties = properties;
|
||||
this.stage = new Stage();
|
||||
buildUI();
|
||||
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) {
|
||||
@@ -74,7 +84,7 @@ public class MainWindow {
|
||||
}
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
private void buildUI(Runnable openSettings) {
|
||||
setAppIcon();
|
||||
stage.setTitle("WorkUtils — 开发工作集");
|
||||
stage.setWidth(1280);
|
||||
@@ -84,7 +94,7 @@ public class MainWindow {
|
||||
stage.setOnCloseRequest(event -> embeddedCache.clear());
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setTop(buildUnifiedHeader());
|
||||
root.setTop(buildUnifiedHeader(openSettings));
|
||||
|
||||
navigationRail = new NavigationRail(navItems, this::navigateTo);
|
||||
contentArea = new StackPane();
|
||||
@@ -100,8 +110,9 @@ public class MainWindow {
|
||||
showWelcomePage();
|
||||
}
|
||||
|
||||
private HBox buildUnifiedHeader() {
|
||||
private HBox buildUnifiedHeader(Runnable openSettings) {
|
||||
HBox bar = new HBox(Theme.S1);
|
||||
bar.setId("app-header");
|
||||
bar.setAlignment(Pos.CENTER_LEFT);
|
||||
bar.setPadding(new Insets(0, Theme.S5, 0, Theme.S4));
|
||||
bar.setMinHeight(DesignTokens.HEADER_HEIGHT);
|
||||
@@ -109,26 +120,38 @@ public class MainWindow {
|
||||
bar.setMaxHeight(DesignTokens.HEADER_HEIGHT);
|
||||
bar.getStyleClass().add(Theme.CSS_HEADER_BAR);
|
||||
|
||||
FontIcon brand = IconFactory.create(AppIcon.APP, 20);
|
||||
bar.getChildren().add(brand);
|
||||
Button menuButton = new Button(null, IconFactory.createButtonIcon(AppIcon.MENU, "切换导航栏"));
|
||||
menuButton.setId("menu-button");
|
||||
menuButton.setTooltip(new Tooltip("切换导航栏"));
|
||||
menuButton.setOnAction(event -> navigationRail.toggle());
|
||||
|
||||
Region spacerLeft = new Region();
|
||||
bar.getChildren().add(spacerLeft);
|
||||
Label brand = new Label("WorkUtils", IconFactory.create(AppIcon.APP, 20));
|
||||
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);
|
||||
toolbarTitle = new Label();
|
||||
toolbarTitle.setFont(Theme.body());
|
||||
pluginToolbar.getChildren().addAll(toolbarIcon, toolbarTitle);
|
||||
bar.getChildren().add(pluginToolbar);
|
||||
toolbarTitle.setFont(Theme.pageTitle());
|
||||
toolbarTitle.getStyleClass().add(Theme.CSS_PAGE_TITLE);
|
||||
pluginToolbar.getChildren().addAll(chevron, toolbarIcon, toolbarTitle);
|
||||
|
||||
Region spacerRight = new Region();
|
||||
HBox.setHgrow(spacerRight, Priority.ALWAYS);
|
||||
bar.getChildren().add(spacerRight);
|
||||
|
||||
Button settingsButton = new Button(
|
||||
null, IconFactory.createButtonIcon(AppIcon.SETTINGS, "系统设置"));
|
||||
settingsButton.setId("settings-button");
|
||||
settingsButton.setTooltip(new Tooltip("系统设置"));
|
||||
settingsButton.setOnAction(event -> openSettings.run());
|
||||
|
||||
bar.getChildren().addAll(
|
||||
menuButton, brand, pluginToolbar, spacerRight, settingsButton);
|
||||
return bar;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.workutils.system.ui.shell;
|
||||
|
||||
import com.example.workutils.system.ui.Theme;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/** Semantic title block for plugin content pages. */
|
||||
public final class PageHeader extends VBox {
|
||||
|
||||
public PageHeader(String title) {
|
||||
this(title, null);
|
||||
}
|
||||
|
||||
public PageHeader(String title, String description) {
|
||||
super(Theme.S1);
|
||||
|
||||
Label titleLabel = new Label(Objects.requireNonNull(title, "title must not be null"));
|
||||
titleLabel.setFont(Theme.pageTitle());
|
||||
titleLabel.getStyleClass().add(Theme.CSS_PAGE_TITLE);
|
||||
getChildren().add(titleLabel);
|
||||
|
||||
if (description != null && !description.isBlank()) {
|
||||
Label descriptionLabel = new Label(description);
|
||||
descriptionLabel.setFont(Theme.secondary());
|
||||
descriptionLabel.getStyleClass().add(Theme.CSS_SECONDARY_TEXT);
|
||||
getChildren().add(descriptionLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ class DefaultPluginConfigurationTest {
|
||||
PluginManager manager = new PluginManager(plugins, properties);
|
||||
manager.init();
|
||||
|
||||
assertEquals(7, manager.getEnabledPlugins().size());
|
||||
assertEquals(
|
||||
List.of("scan", "diff", "api-doc", "git-sync", "git-review",
|
||||
"mybatisexport", "mybatisgenerator"),
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.example.workutils.system.ui;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MainWindowArchitectureTest {
|
||||
|
||||
@Test
|
||||
void settingsIsAUniqueHeaderActionInsteadOfANavigationItem() throws Exception {
|
||||
String application = source("src/main/java/com/example/workutils/WorkUtilsApplication.java");
|
||||
String buildNavItems = between(application,
|
||||
"private List<MainWindow.NavItem> buildNavItems()",
|
||||
"// ==================== 内嵌优先打开");
|
||||
String mainWindow = source("src/main/java/com/example/workutils/system/ui/MainWindow.java");
|
||||
|
||||
assertFalse(buildNavItems.contains("__settings__"));
|
||||
assertFalse(buildNavItems.contains("系统设置"));
|
||||
assertTrue(application.contains("mainWindow.refreshNavItems(buildNavItems())"));
|
||||
assertTrue(application.contains(
|
||||
"new MainWindow(navItems, properties, this::openSettingsEmbedded)"));
|
||||
assertTrue(mainWindow.contains(
|
||||
"MainWindow(List<NavItem> navItems, WorkUtilsProperties properties, Runnable openSettings)"));
|
||||
assertTrue(mainWindow.contains("IconFactory.createButtonIcon(AppIcon.SETTINGS"));
|
||||
assertTrue(mainWindow.contains("openSettings.run()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void headerUsesDesignTokensAndFeatherSemanticsWithoutCustomWindowChrome() throws Exception {
|
||||
String mainWindow = source("src/main/java/com/example/workutils/system/ui/MainWindow.java");
|
||||
String header = between(mainWindow, "private HBox buildUnifiedHeader", "private void navigateTo");
|
||||
|
||||
assertTrue(header.contains("DesignTokens.HEADER_HEIGHT"));
|
||||
assertTrue(header.contains("IconFactory.createButtonIcon(AppIcon.MENU"));
|
||||
assertTrue(header.contains("navigationRail.toggle()"));
|
||||
assertTrue(header.contains("IconFactory.create(AppIcon.APP"));
|
||||
assertTrue(header.contains("IconFactory.create(AppIcon.CHEVRON_RIGHT"));
|
||||
assertTrue(header.contains("IconFactory.createButtonIcon(AppIcon.SETTINGS"));
|
||||
assertEquals(1, occurrences(header, "AppIcon.SETTINGS"));
|
||||
assertEquals(1, occurrences(header, "openSettings.run()"));
|
||||
assertFalse(header.contains("SVGPath"));
|
||||
assertFalse(header.contains("<svg"));
|
||||
assertFalse(mainWindow.contains("StageStyle.UNDECORATED"));
|
||||
assertFalse(mainWindow.contains("initStyle("));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageHeaderExposesSemanticTitleAndOptionalDescription() throws Exception {
|
||||
String pageHeader = source(
|
||||
"src/main/java/com/example/workutils/system/ui/shell/PageHeader.java");
|
||||
|
||||
assertTrue(pageHeader.contains("Theme.CSS_PAGE_TITLE"));
|
||||
assertTrue(pageHeader.contains("Theme.CSS_SECONDARY_TEXT"));
|
||||
assertTrue(pageHeader.contains("description.isBlank()"));
|
||||
}
|
||||
|
||||
private static String source(String path) throws Exception {
|
||||
return Files.readString(Path.of(path));
|
||||
}
|
||||
|
||||
private static String between(String source, String start, String end) {
|
||||
int startIndex = source.indexOf(start);
|
||||
int endIndex = source.indexOf(end, startIndex);
|
||||
assertTrue(startIndex >= 0, () -> "Missing source marker: " + start);
|
||||
assertTrue(endIndex > startIndex, () -> "Missing source marker: " + end);
|
||||
return source.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
private static int occurrences(String source, String needle) {
|
||||
return source.split(java.util.regex.Pattern.quote(needle), -1).length - 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user