修复导航语义图标与刷新状态
This commit is contained in:
@@ -40,7 +40,7 @@ public class MainWindow {
|
|||||||
private EmbeddedEntry currentEmbedded;
|
private EmbeddedEntry currentEmbedded;
|
||||||
|
|
||||||
private HBox pluginToolbar;
|
private HBox pluginToolbar;
|
||||||
private Label toolbarIcon;
|
private FontIcon toolbarIcon;
|
||||||
private Label toolbarTitle;
|
private Label toolbarTitle;
|
||||||
|
|
||||||
public MainWindow(List<NavItem> navItems, WorkUtilsProperties properties) {
|
public MainWindow(List<NavItem> navItems, WorkUtilsProperties properties) {
|
||||||
@@ -120,8 +120,7 @@ public class MainWindow {
|
|||||||
pluginToolbar.setVisible(false);
|
pluginToolbar.setVisible(false);
|
||||||
pluginToolbar.setManaged(false);
|
pluginToolbar.setManaged(false);
|
||||||
|
|
||||||
toolbarIcon = new Label();
|
toolbarIcon = IconFactory.create(AppIcon.APP, 18);
|
||||||
toolbarIcon.setFont(Theme.body());
|
|
||||||
toolbarTitle = new Label();
|
toolbarTitle = new Label();
|
||||||
toolbarTitle.setFont(Theme.body());
|
toolbarTitle.setFont(Theme.body());
|
||||||
pluginToolbar.getChildren().addAll(toolbarIcon, toolbarTitle);
|
pluginToolbar.getChildren().addAll(toolbarIcon, toolbarTitle);
|
||||||
@@ -141,8 +140,8 @@ public class MainWindow {
|
|||||||
showEmbeddedFor(item);
|
showEmbeddedFor(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showPluginToolbar(String icon, String title) {
|
private void showPluginToolbar(AppIcon icon, String title) {
|
||||||
toolbarIcon.setText(icon);
|
toolbarIcon.setIconCode(icon.glyph());
|
||||||
toolbarTitle.setText(title);
|
toolbarTitle.setText(title);
|
||||||
pluginToolbar.setVisible(true);
|
pluginToolbar.setVisible(true);
|
||||||
pluginToolbar.setManaged(true);
|
pluginToolbar.setManaged(true);
|
||||||
@@ -191,9 +190,9 @@ public class MainWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void embedPlugin(String pluginId, String icon, String name, Parent content) {
|
public void embedPlugin(String pluginId, String ignoredIcon, String name, Parent content) {
|
||||||
embeddedCache.remove(pluginId);
|
embeddedCache.remove(pluginId);
|
||||||
EmbeddedEntry entry = new EmbeddedEntry(content, icon, name);
|
EmbeddedEntry entry = new EmbeddedEntry(content, NavItem.iconForPlugin(pluginId), name);
|
||||||
embeddedCache.put(pluginId, entry);
|
embeddedCache.put(pluginId, entry);
|
||||||
switchToEmbedded(pluginId, entry);
|
switchToEmbedded(pluginId, entry);
|
||||||
}
|
}
|
||||||
@@ -242,7 +241,7 @@ public class MainWindow {
|
|||||||
wrapper.getStyleClass().add(Theme.CSS_CONTENT_AREA);
|
wrapper.getStyleClass().add(Theme.CSS_CONTENT_AREA);
|
||||||
wrapper.setCenter(content);
|
wrapper.setCenter(content);
|
||||||
contentArea.getChildren().setAll(wrapper);
|
contentArea.getChildren().setAll(wrapper);
|
||||||
showPluginToolbar(icon, title);
|
showPluginToolbar(AppIcon.APP, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshTheme() {
|
public void refreshTheme() {
|
||||||
@@ -251,11 +250,11 @@ public class MainWindow {
|
|||||||
|
|
||||||
private static final class EmbeddedEntry {
|
private static final class EmbeddedEntry {
|
||||||
private final Parent content;
|
private final Parent content;
|
||||||
private final String icon;
|
private final AppIcon icon;
|
||||||
private final String title;
|
private final String title;
|
||||||
private BorderPane wrapper;
|
private BorderPane wrapper;
|
||||||
|
|
||||||
private EmbeddedEntry(Parent content, String icon, String title) {
|
private EmbeddedEntry(Parent content, AppIcon icon, String title) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
@@ -303,6 +302,7 @@ public class MainWindow {
|
|||||||
case "git-review" -> AppIcon.GIT_REVIEW;
|
case "git-review" -> AppIcon.GIT_REVIEW;
|
||||||
case "mybatisexport" -> AppIcon.SQL_EXPORT;
|
case "mybatisexport" -> AppIcon.SQL_EXPORT;
|
||||||
case "mybatisgenerator" -> AppIcon.MYBATIS_GENERATOR;
|
case "mybatisgenerator" -> AppIcon.MYBATIS_GENERATOR;
|
||||||
|
case "mobile-ui-auto" -> AppIcon.APP;
|
||||||
case "__settings__" -> AppIcon.SETTINGS;
|
case "__settings__" -> AppIcon.SETTINGS;
|
||||||
default -> AppIcon.APP;
|
default -> AppIcon.APP;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import javafx.scene.layout.Priority;
|
|||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -37,9 +38,8 @@ public final class NavigationRail extends VBox {
|
|||||||
private final ReadOnlyBooleanWrapper expanded = new ReadOnlyBooleanWrapper(this, "expanded", true);
|
private final ReadOnlyBooleanWrapper expanded = new ReadOnlyBooleanWrapper(this, "expanded", true);
|
||||||
private final Consumer<MainWindow.NavItem> navigator;
|
private final Consumer<MainWindow.NavItem> navigator;
|
||||||
private final ToggleGroup selection = new ToggleGroup();
|
private final ToggleGroup selection = new ToggleGroup();
|
||||||
private final Map<String, MainWindow.NavItem> itemsByPluginId = new LinkedHashMap<>();
|
private final List<ItemView> itemViews = new ArrayList<>();
|
||||||
private final Map<String, ToggleButton> buttonsByPluginId = new LinkedHashMap<>();
|
private final Map<String, ItemView> viewsByPluginId = new LinkedHashMap<>();
|
||||||
private final Map<String, Tooltip> tooltipsByPluginId = new LinkedHashMap<>();
|
|
||||||
private final VBox itemBox = new VBox();
|
private final VBox itemBox = new VBox();
|
||||||
private final Label brandLabel = new Label("WorkUtils");
|
private final Label brandLabel = new Label("WorkUtils");
|
||||||
private final Button collapseButton = new Button();
|
private final Button collapseButton = new Button();
|
||||||
@@ -83,12 +83,13 @@ public final class NavigationRail extends VBox {
|
|||||||
|
|
||||||
public void selectPlugin(String pluginId) {
|
public void selectPlugin(String pluginId) {
|
||||||
requireFxThread();
|
requireFxThread();
|
||||||
ToggleButton button = buttonsByPluginId.get(pluginId);
|
if (pluginId == null) {
|
||||||
MainWindow.NavItem item = itemsByPluginId.get(pluginId);
|
|
||||||
if (button == null || item == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activate(item, button);
|
ItemView view = viewsByPluginId.get(pluginId);
|
||||||
|
if (view != null) {
|
||||||
|
activate(view.item(), view.button());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String selectedPluginId() {
|
public String selectedPluginId() {
|
||||||
@@ -100,21 +101,26 @@ public final class NavigationRail extends VBox {
|
|||||||
Objects.requireNonNull(items, "items must not be null");
|
Objects.requireNonNull(items, "items must not be null");
|
||||||
|
|
||||||
selection.selectToggle(null);
|
selection.selectToggle(null);
|
||||||
|
for (ItemView view : itemViews) {
|
||||||
|
view.button().setOnAction(null);
|
||||||
|
view.button().setTooltip(null);
|
||||||
|
view.button().setToggleGroup(null);
|
||||||
|
}
|
||||||
itemBox.getChildren().clear();
|
itemBox.getChildren().clear();
|
||||||
itemsByPluginId.clear();
|
itemViews.clear();
|
||||||
buttonsByPluginId.clear();
|
viewsByPluginId.clear();
|
||||||
tooltipsByPluginId.clear();
|
|
||||||
selectedItem = null;
|
selectedItem = null;
|
||||||
selectedPluginId = null;
|
selectedPluginId = null;
|
||||||
|
|
||||||
for (MainWindow.NavItem item : items) {
|
for (int index = 0; index < items.size(); index++) {
|
||||||
addItem(Objects.requireNonNull(item, "navigation item must not be null"));
|
addItem(Objects.requireNonNull(items.get(index), "navigation item must not be null"), index);
|
||||||
}
|
}
|
||||||
updateItemAppearance();
|
updateItemAppearance();
|
||||||
}
|
}
|
||||||
|
|
||||||
Tooltip tooltipFor(String pluginId) {
|
Tooltip tooltipFor(String pluginId) {
|
||||||
return tooltipsByPluginId.get(pluginId);
|
ItemView view = pluginId == null ? null : viewsByPluginId.get(pluginId);
|
||||||
|
return view == null ? null : view.tooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildHeader() {
|
private void buildHeader() {
|
||||||
@@ -137,7 +143,7 @@ public final class NavigationRail extends VBox {
|
|||||||
getChildren().add(header);
|
getChildren().add(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addItem(MainWindow.NavItem item) {
|
private void addItem(MainWindow.NavItem item, int index) {
|
||||||
ToggleButton button = new ToggleButton(item.name, IconFactory.createButtonIcon(item.icon, item.name));
|
ToggleButton button = new ToggleButton(item.name, IconFactory.createButtonIcon(item.icon, item.name));
|
||||||
button.setToggleGroup(selection);
|
button.setToggleGroup(selection);
|
||||||
button.setContentDisplay(ContentDisplay.LEFT);
|
button.setContentDisplay(ContentDisplay.LEFT);
|
||||||
@@ -149,21 +155,24 @@ public final class NavigationRail extends VBox {
|
|||||||
button.getStyleClass().add(Theme.CSS_NAV_CELL);
|
button.getStyleClass().add(Theme.CSS_NAV_CELL);
|
||||||
if (item.pluginId != null) {
|
if (item.pluginId != null) {
|
||||||
button.setId("nav-" + item.pluginId);
|
button.setId("nav-" + item.pluginId);
|
||||||
|
} else {
|
||||||
|
button.setId("nav-item-" + index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tooltip tooltip = new Tooltip(item.name);
|
Tooltip tooltip = new Tooltip(item.name);
|
||||||
button.setOnAction(event -> activate(item, button));
|
button.setOnAction(event -> activate(item, button));
|
||||||
|
|
||||||
itemsByPluginId.put(item.pluginId, item);
|
ItemView view = new ItemView(item, button, tooltip);
|
||||||
buttonsByPluginId.put(item.pluginId, button);
|
itemViews.add(view);
|
||||||
tooltipsByPluginId.put(item.pluginId, tooltip);
|
if (item.pluginId != null) {
|
||||||
|
viewsByPluginId.put(item.pluginId, view);
|
||||||
|
}
|
||||||
itemBox.getChildren().add(button);
|
itemBox.getChildren().add(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void activate(MainWindow.NavItem item, ToggleButton button) {
|
private void activate(MainWindow.NavItem item, ToggleButton button) {
|
||||||
selection.selectToggle(button);
|
selection.selectToggle(button);
|
||||||
buttonsByPluginId.values().forEach(candidate ->
|
itemViews.forEach(view -> view.button().getStyleClass().remove(Theme.CSS_NAV_CELL_ACTIVE));
|
||||||
candidate.getStyleClass().remove(Theme.CSS_NAV_CELL_ACTIVE));
|
|
||||||
if (!button.getStyleClass().contains(Theme.CSS_NAV_CELL_ACTIVE)) {
|
if (!button.getStyleClass().contains(Theme.CSS_NAV_CELL_ACTIVE)) {
|
||||||
button.getStyleClass().add(Theme.CSS_NAV_CELL_ACTIVE);
|
button.getStyleClass().add(Theme.CSS_NAV_CELL_ACTIVE);
|
||||||
}
|
}
|
||||||
@@ -194,15 +203,18 @@ public final class NavigationRail extends VBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateItemAppearance() {
|
private void updateItemAppearance() {
|
||||||
for (Map.Entry<String, ToggleButton> entry : buttonsByPluginId.entrySet()) {
|
for (ItemView view : itemViews) {
|
||||||
MainWindow.NavItem item = itemsByPluginId.get(entry.getKey());
|
MainWindow.NavItem item = view.item();
|
||||||
ToggleButton button = entry.getValue();
|
ToggleButton button = view.button();
|
||||||
button.setText(isExpanded() ? item.name : null);
|
button.setText(isExpanded() ? item.name : null);
|
||||||
button.setTooltip(isExpanded() ? null : tooltipsByPluginId.get(entry.getKey()));
|
button.setTooltip(isExpanded() ? null : view.tooltip());
|
||||||
button.setAlignment(isExpanded() ? Pos.CENTER_LEFT : Pos.CENTER);
|
button.setAlignment(isExpanded() ? Pos.CENTER_LEFT : Pos.CENTER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record ItemView(MainWindow.NavItem item, ToggleButton button, Tooltip tooltip) {
|
||||||
|
}
|
||||||
|
|
||||||
private static void requireFxThread() {
|
private static void requireFxThread() {
|
||||||
if (!Platform.isFxApplicationThread()) {
|
if (!Platform.isFxApplicationThread()) {
|
||||||
throw new IllegalStateException("NavigationRail must be accessed on the JavaFX Application Thread");
|
throw new IllegalStateException("NavigationRail must be accessed on the JavaFX Application Thread");
|
||||||
|
|||||||
@@ -4,17 +4,25 @@ import com.example.workutils.system.ui.DesignTokens;
|
|||||||
import com.example.workutils.system.ui.MainWindow;
|
import com.example.workutils.system.ui.MainWindow;
|
||||||
import com.example.workutils.system.ui.Theme;
|
import com.example.workutils.system.ui.Theme;
|
||||||
import com.example.workutils.system.ui.icon.AppIcon;
|
import com.example.workutils.system.ui.icon.AppIcon;
|
||||||
|
import com.example.workutils.system.config.WorkUtilsProperties;
|
||||||
import javafx.css.PseudoClass;
|
import javafx.css.PseudoClass;
|
||||||
import javafx.scene.control.ToggleButton;
|
import javafx.scene.control.ToggleButton;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static com.example.workutils.system.ui.FxTestSupport.fx;
|
import static com.example.workutils.system.ui.FxTestSupport.fx;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -29,6 +37,22 @@ class NavigationRailTest {
|
|||||||
},
|
},
|
||||||
"api-doc");
|
"api-doc");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pluginToolbarUsesFeatherSemanticsInsteadOfLegacyTextIcons() throws Exception {
|
||||||
|
fx(() -> {
|
||||||
|
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());
|
||||||
|
|
||||||
|
window.embedPlugin("mobile-ui-auto", "legacy-mobile-icon", "Mobile", new StackPane());
|
||||||
|
assertEquals(AppIcon.APP.glyph(), toolbarIcon(window).getIconCode());
|
||||||
|
|
||||||
|
window.embedPlugin("third-party", "legacy-third-party-icon", "Other", new StackPane());
|
||||||
|
assertEquals(AppIcon.APP.glyph(), toolbarIcon(window).getIconCode());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void collapsePreservesSelectionAndDoesNotRouteAgain() throws Exception {
|
void collapsePreservesSelectionAndDoesNotRouteAgain() throws Exception {
|
||||||
fx(() -> {
|
fx(() -> {
|
||||||
@@ -67,6 +91,79 @@ class NavigationRailTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void itemsWithoutPluginIdsKeepIndependentCollapseAndActiveState() throws Exception {
|
||||||
|
fx(() -> {
|
||||||
|
MainWindow.NavItem first = new MainWindow.NavItem(
|
||||||
|
AppIcon.DIFF, "First", "First item", () -> {
|
||||||
|
});
|
||||||
|
MainWindow.NavItem second = new MainWindow.NavItem(
|
||||||
|
AppIcon.SCANNER, "Second", "Second item", () -> {
|
||||||
|
});
|
||||||
|
List<MainWindow.NavItem> routed = new ArrayList<>();
|
||||||
|
NavigationRail rail = new NavigationRail(List.of(first, second), routed::add);
|
||||||
|
|
||||||
|
rail.setExpanded(false);
|
||||||
|
ToggleButton firstButton = (ToggleButton) rail.lookup("#nav-item-0");
|
||||||
|
ToggleButton secondButton = (ToggleButton) rail.lookup("#nav-item-1");
|
||||||
|
|
||||||
|
assertNotNull(firstButton);
|
||||||
|
assertNotNull(secondButton);
|
||||||
|
assertEquals("First", firstButton.getTooltip().getText());
|
||||||
|
assertEquals("Second", secondButton.getTooltip().getText());
|
||||||
|
|
||||||
|
firstButton.fire();
|
||||||
|
secondButton.fire();
|
||||||
|
|
||||||
|
assertEquals(List.of(first, second), routed);
|
||||||
|
assertTrue(secondButton.getStyleClass().contains(Theme.CSS_NAV_CELL_ACTIVE));
|
||||||
|
assertFalse(firstButton.getStyleClass().contains(Theme.CSS_NAV_CELL_ACTIVE));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullAndUnknownPluginIdsDoNotSelectOrRoute() throws Exception {
|
||||||
|
fx(() -> {
|
||||||
|
AtomicInteger routed = new AtomicInteger();
|
||||||
|
MainWindow.NavItem withoutId = new MainWindow.NavItem(
|
||||||
|
AppIcon.APP, "No ID", "No ID item", () -> {
|
||||||
|
});
|
||||||
|
NavigationRail rail = new NavigationRail(List.of(withoutId, API_DOC),
|
||||||
|
item -> routed.incrementAndGet());
|
||||||
|
|
||||||
|
rail.selectPlugin(null);
|
||||||
|
rail.selectPlugin("unknown-plugin");
|
||||||
|
|
||||||
|
assertEquals(0, routed.get());
|
||||||
|
assertNull(rail.selectedPluginId());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refreshDetachesOldButtonsFromSelectionAndRouting() throws Exception {
|
||||||
|
fx(() -> {
|
||||||
|
AtomicInteger routed = new AtomicInteger();
|
||||||
|
MainWindow.NavItem first = new MainWindow.NavItem(
|
||||||
|
AppIcon.DIFF, "First", "First item", () -> {
|
||||||
|
}, "first");
|
||||||
|
MainWindow.NavItem second = new MainWindow.NavItem(
|
||||||
|
AppIcon.SCANNER, "Second", "Second item", () -> {
|
||||||
|
}, "second");
|
||||||
|
NavigationRail rail = new NavigationRail(List.of(first), item -> routed.incrementAndGet());
|
||||||
|
ToggleButton oldButton = (ToggleButton) rail.lookup("#nav-first");
|
||||||
|
oldButton.fire();
|
||||||
|
|
||||||
|
rail.setItems(List.of(second));
|
||||||
|
ToggleButton newButton = (ToggleButton) rail.lookup("#nav-second");
|
||||||
|
|
||||||
|
assertNull(oldButton.getToggleGroup());
|
||||||
|
assertNull(oldButton.getOnAction());
|
||||||
|
assertEquals(1, newButton.getToggleGroup().getToggles().size());
|
||||||
|
oldButton.fire();
|
||||||
|
assertEquals(1, routed.get());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void collapsedRailUsesExactFixedWidthAndCollapsedPseudoClass() throws Exception {
|
void collapsedRailUsesExactFixedWidthAndCollapsedPseudoClass() throws Exception {
|
||||||
fx(() -> {
|
fx(() -> {
|
||||||
@@ -100,4 +197,10 @@ class NavigationRailTest {
|
|||||||
assertSame(tooltip, apiDocButton.getTooltip());
|
assertSame(tooltip, apiDocButton.getTooltip());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static FontIcon toolbarIcon(MainWindow window) throws ReflectiveOperationException {
|
||||||
|
Field field = MainWindow.class.getDeclaredField("toolbarIcon");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return assertInstanceOf(FontIcon.class, field.get(window));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user