统一应用顶部栏与设置入口
This commit is contained in:
@@ -111,9 +111,9 @@ public class WorkUtilsApplication implements CommandLineRunner {
|
|||||||
Platform.runLater(Theme::initTheme);
|
Platform.runLater(Theme::initTheme);
|
||||||
|
|
||||||
// 设置导航栏刷新回调(插件管理中启用/禁用后刷新侧边栏)
|
// 设置导航栏刷新回调(插件管理中启用/禁用后刷新侧边栏)
|
||||||
settingsService.setNavItemsRefresher(items -> Platform.runLater(() -> {
|
settingsService.setNavItemsRefresher(ignoredItems -> Platform.runLater(() -> {
|
||||||
if (mainWindow != null) {
|
if (mainWindow != null) {
|
||||||
mainWindow.refreshNavItems(items);
|
mainWindow.refreshNavItems(buildNavItems());
|
||||||
// 重新绑定插件面板
|
// 重新绑定插件面板
|
||||||
for (Plugin plugin : pluginManager.getEnabledPlugins()) {
|
for (Plugin plugin : pluginManager.getEnabledPlugins()) {
|
||||||
PluginDescriptor desc = plugin.getDescriptor();
|
PluginDescriptor desc = plugin.getDescriptor();
|
||||||
@@ -131,7 +131,7 @@ public class WorkUtilsApplication implements CommandLineRunner {
|
|||||||
CountDownLatch windowClosed = new CountDownLatch(1);
|
CountDownLatch windowClosed = new CountDownLatch(1);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
try {
|
try {
|
||||||
mainWindow = new MainWindow(navItems, properties);
|
mainWindow = new MainWindow(navItems, properties, this::openSettingsEmbedded);
|
||||||
mainWindow.showAndWait();
|
mainWindow.showAndWait();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("主窗口创建失败", e);
|
log.error("主窗口创建失败", e);
|
||||||
@@ -164,14 +164,6 @@ public class WorkUtilsApplication implements CommandLineRunner {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 系统设置 — 内嵌为 Tab
|
|
||||||
items.add(new MainWindow.NavItem(
|
|
||||||
"⚙️", "系统设置",
|
|
||||||
"配置系统路径、数据库连接、项目路径、多数据源、字体大小等",
|
|
||||||
this::openSettingsEmbedded,
|
|
||||||
"__settings__"
|
|
||||||
));
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import javafx.geometry.Insets;
|
|||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ContentDisplay;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
@@ -25,6 +28,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/** Main application window and shell-level plugin content host. */
|
/** Main application window and shell-level plugin content host. */
|
||||||
public class MainWindow {
|
public class MainWindow {
|
||||||
@@ -43,11 +47,17 @@ public class MainWindow {
|
|||||||
private FontIcon toolbarIcon;
|
private FontIcon toolbarIcon;
|
||||||
private Label toolbarTitle;
|
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.navItems = new ArrayList<>(navItems);
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.stage = new Stage();
|
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) {
|
public void refreshNavItems(List<NavItem> newItems) {
|
||||||
@@ -74,7 +84,7 @@ public class MainWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildUI() {
|
private void buildUI(Runnable openSettings) {
|
||||||
setAppIcon();
|
setAppIcon();
|
||||||
stage.setTitle("WorkUtils — 开发工作集");
|
stage.setTitle("WorkUtils — 开发工作集");
|
||||||
stage.setWidth(1280);
|
stage.setWidth(1280);
|
||||||
@@ -84,7 +94,7 @@ public class MainWindow {
|
|||||||
stage.setOnCloseRequest(event -> embeddedCache.clear());
|
stage.setOnCloseRequest(event -> embeddedCache.clear());
|
||||||
|
|
||||||
BorderPane root = new BorderPane();
|
BorderPane root = new BorderPane();
|
||||||
root.setTop(buildUnifiedHeader());
|
root.setTop(buildUnifiedHeader(openSettings));
|
||||||
|
|
||||||
navigationRail = new NavigationRail(navItems, this::navigateTo);
|
navigationRail = new NavigationRail(navItems, this::navigateTo);
|
||||||
contentArea = new StackPane();
|
contentArea = new StackPane();
|
||||||
@@ -100,8 +110,9 @@ public class MainWindow {
|
|||||||
showWelcomePage();
|
showWelcomePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox buildUnifiedHeader() {
|
private HBox buildUnifiedHeader(Runnable openSettings) {
|
||||||
HBox bar = new HBox(Theme.S1);
|
HBox bar = new HBox(Theme.S1);
|
||||||
|
bar.setId("app-header");
|
||||||
bar.setAlignment(Pos.CENTER_LEFT);
|
bar.setAlignment(Pos.CENTER_LEFT);
|
||||||
bar.setPadding(new Insets(0, Theme.S5, 0, Theme.S4));
|
bar.setPadding(new Insets(0, Theme.S5, 0, Theme.S4));
|
||||||
bar.setMinHeight(DesignTokens.HEADER_HEIGHT);
|
bar.setMinHeight(DesignTokens.HEADER_HEIGHT);
|
||||||
@@ -109,26 +120,38 @@ public class MainWindow {
|
|||||||
bar.setMaxHeight(DesignTokens.HEADER_HEIGHT);
|
bar.setMaxHeight(DesignTokens.HEADER_HEIGHT);
|
||||||
bar.getStyleClass().add(Theme.CSS_HEADER_BAR);
|
bar.getStyleClass().add(Theme.CSS_HEADER_BAR);
|
||||||
|
|
||||||
FontIcon brand = IconFactory.create(AppIcon.APP, 20);
|
Button menuButton = new Button(null, IconFactory.createButtonIcon(AppIcon.MENU, "切换导航栏"));
|
||||||
bar.getChildren().add(brand);
|
menuButton.setId("menu-button");
|
||||||
|
menuButton.setTooltip(new Tooltip("切换导航栏"));
|
||||||
|
menuButton.setOnAction(event -> navigationRail.toggle());
|
||||||
|
|
||||||
Region spacerLeft = new Region();
|
Label brand = new Label("WorkUtils", IconFactory.create(AppIcon.APP, 20));
|
||||||
bar.getChildren().add(spacerLeft);
|
brand.setContentDisplay(ContentDisplay.LEFT);
|
||||||
|
brand.setFont(Theme.sectionTitle());
|
||||||
|
|
||||||
pluginToolbar = new HBox(Theme.S2);
|
pluginToolbar = new HBox(Theme.S2);
|
||||||
pluginToolbar.setAlignment(Pos.CENTER_LEFT);
|
pluginToolbar.setAlignment(Pos.CENTER_LEFT);
|
||||||
pluginToolbar.setVisible(false);
|
pluginToolbar.setVisible(false);
|
||||||
pluginToolbar.setManaged(false);
|
pluginToolbar.setManaged(false);
|
||||||
|
|
||||||
|
FontIcon chevron = IconFactory.create(AppIcon.CHEVRON_RIGHT, 14);
|
||||||
toolbarIcon = IconFactory.create(AppIcon.APP, 18);
|
toolbarIcon = IconFactory.create(AppIcon.APP, 18);
|
||||||
toolbarTitle = new Label();
|
toolbarTitle = new Label();
|
||||||
toolbarTitle.setFont(Theme.body());
|
toolbarTitle.setFont(Theme.pageTitle());
|
||||||
pluginToolbar.getChildren().addAll(toolbarIcon, toolbarTitle);
|
toolbarTitle.getStyleClass().add(Theme.CSS_PAGE_TITLE);
|
||||||
bar.getChildren().add(pluginToolbar);
|
pluginToolbar.getChildren().addAll(chevron, toolbarIcon, toolbarTitle);
|
||||||
|
|
||||||
Region spacerRight = new Region();
|
Region spacerRight = new Region();
|
||||||
HBox.setHgrow(spacerRight, Priority.ALWAYS);
|
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;
|
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);
|
PluginManager manager = new PluginManager(plugins, properties);
|
||||||
manager.init();
|
manager.init();
|
||||||
|
|
||||||
|
assertEquals(7, manager.getEnabledPlugins().size());
|
||||||
assertEquals(
|
assertEquals(
|
||||||
List.of("scan", "diff", "api-doc", "git-sync", "git-review",
|
List.of("scan", "diff", "api-doc", "git-sync", "git-review",
|
||||||
"mybatisexport", "mybatisgenerator"),
|
"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