feat(shell): add search/clear icons and Escape-to-cancel to search bar
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzvA5ySQUsYrMUTj7sHxFA
This commit is contained in:
@@ -1,18 +1,21 @@
|
||||
package org.icroco.pholio.ui.shell;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import com.dlsc.gemsfx.CustomTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.HeaderBar;
|
||||
import javafx.scene.layout.HeaderDragType;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Duration;
|
||||
import org.icroco.pholio.infra.i18n.I18nService;
|
||||
@@ -29,6 +32,8 @@ import org.icroco.pholio.ui.theme.EColorMode;
|
||||
import org.icroco.pholio.ui.theme.ThemeManager;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
import org.kordamp.ikonli.materialdesign2.MaterialDesignC;
|
||||
import org.kordamp.ikonli.materialdesign2.MaterialDesignM;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import java.io.File;
|
||||
@@ -79,7 +84,9 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
private final MenuButton librarySelector = new MenuButton();
|
||||
private final Button deleteLibraryButton = new Button();
|
||||
private final Button importButton = new Button();
|
||||
private final TextField search = new TextField();
|
||||
private final CustomTextField search = new CustomTextField();
|
||||
private final FontIcon searchIcon = new FontIcon(MaterialDesignM.MAGNIFY);
|
||||
private final FontIcon searchClearIcon = new FontIcon(MaterialDesignC.CLOSE_CIRCLE);
|
||||
private final PauseTransition searchDebounce = new PauseTransition(Duration.millis(250));
|
||||
|
||||
public AppHeaderBar(ApplicationEventPublisher publisher,
|
||||
@@ -98,8 +105,8 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
this.gallerySearchState = gallerySearchState;
|
||||
|
||||
getStyleClass().add("app-header-bar");
|
||||
HBox brand = buildBrand();
|
||||
TextField search = buildSearch();
|
||||
HBox brand = buildBrand();
|
||||
CustomTextField search = buildSearch();
|
||||
|
||||
setLeft(brand);
|
||||
setCenter(search);
|
||||
@@ -120,7 +127,9 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
// ever starts, and how much varies by platform (and disappears in fullscreen). boundsInParent
|
||||
// already reflects all of that, whatever it happens to be, plus app.name's own translated width.
|
||||
Runnable alignSearchWithGallery = () -> HeaderBar.setMargin(search, new Insets(0, 12, 0,
|
||||
Math.max(0, NavigationSidebar.TOTAL_WIDTH - brand.getBoundsInParent().getMaxX())));
|
||||
Math.max(0,
|
||||
NavigationSidebar.TOTAL_WIDTH -
|
||||
brand.getBoundsInParent().getMaxX())));
|
||||
alignSearchWithGallery.run();
|
||||
brand.boundsInParentProperty().addListener((obs, oldBounds, newBounds) -> alignSearchWithGallery.run());
|
||||
|
||||
@@ -321,7 +330,7 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
* alignment/margin that actually positions it) — no wrapping {@code HBox} needed, since a
|
||||
* {@code CENTER_LEFT}-aligned centre child is resized to fill its whole span on its own.
|
||||
*/
|
||||
private TextField buildSearch() {
|
||||
private CustomTextField buildSearch() {
|
||||
search.promptTextProperty().bind(i18n.binding("action.search.prompt"));
|
||||
// Unstyled, a TextField's own padding makes it taller than the header bar comfortably fits.
|
||||
search.getStyleClass().addAll(Styles.TEXT_NORMAL, Styles.ROUNDED);
|
||||
@@ -330,6 +339,17 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
searchTooltip.textProperty().bind(i18n.binding("action.search.tooltip"));
|
||||
search.setTooltip(searchTooltip);
|
||||
scope.addTeardown(() -> searchTooltip.textProperty().unbind());
|
||||
// Left/right graphics via CustomTextField (gemsfx), not a plain TextField setGraphic — TextField
|
||||
// is a TextInputControl, not a Labeled, so it has no graphic slot of its own to hijack.
|
||||
searchIcon.getStyleClass().add("search-icon");
|
||||
search.setLeft(searchIcon);
|
||||
searchClearIcon.getStyleClass().add("search-clear-icon");
|
||||
searchClearIcon.setCursor(Cursor.HAND);
|
||||
// Only worth showing (and clicking) once there is something to clear.
|
||||
searchClearIcon.visibleProperty().bind(search.textProperty().isNotEmpty());
|
||||
searchClearIcon.managedProperty().bind(searchClearIcon.visibleProperty());
|
||||
searchClearIcon.setOnMouseClicked(event -> search.clear());
|
||||
search.setRight(searchClearIcon);
|
||||
// Debounced, not applied on every keystroke: GallerySearchState.setQuery drives a full re-filter
|
||||
// (and re-chunk) of ThumbnailGalleryPane's grid, too costly to redo on every character typed.
|
||||
searchDebounce.setOnFinished(event -> gallerySearchState.setQuery(search.getText()));
|
||||
@@ -351,6 +371,16 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
gallerySearchState.setQuery(search.getText());
|
||||
publisher.publishEvent(new NavigateToDestinationEvent(ENavigationDestination.PHOTOS));
|
||||
});
|
||||
// Escape cancels whatever is being typed — only consumed when there is text to cancel, so an
|
||||
// empty field still lets Escape bubble up to any other shortcut listening for it.
|
||||
search.setOnKeyPressed(event -> {
|
||||
if (event.getCode() == KeyCode.ESCAPE && !search.getText().isEmpty()) {
|
||||
searchDebounce.stop();
|
||||
search.clear();
|
||||
gallerySearchState.setQuery("");
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
return search;
|
||||
}
|
||||
@@ -361,7 +391,7 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
* matching the sidebar's own visual order of "library concerns first".
|
||||
*/
|
||||
private HBox buildActions() {
|
||||
importButton.setGraphic(FontIcon.of(Feather.PLUS, 18));
|
||||
importButton.setGraphic(FontIcon.of(Feather.PLUS, 20));
|
||||
importButton.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.FLAT);
|
||||
Tooltip importTooltip = new Tooltip();
|
||||
importTooltip.textProperty().bind(i18n.binding("header.import.tooltip"));
|
||||
@@ -375,7 +405,7 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
theme.setOnAction(event -> themeManager.cycle());
|
||||
scope.add(themeManager.colorModeProperty().subscribe(this::updateThemeIcon));
|
||||
|
||||
Button settings = iconButton(FontIcon.of(Feather.SETTINGS, 18), "action.settings");
|
||||
Button settings = iconButton(FontIcon.of(Feather.SETTINGS, 20), "action.settings");
|
||||
settings.setOnAction(event -> modalService.show(SettingsView.class));
|
||||
|
||||
HBox actions = new HBox(4, buildLibrarySelector(), importButton, theme, settings);
|
||||
@@ -418,6 +448,10 @@ public class AppHeaderBar extends HeaderBar implements Disposable {
|
||||
search.promptTextProperty().unbind();
|
||||
search.setTooltip(null);
|
||||
search.setOnAction(null);
|
||||
search.setOnKeyPressed(null);
|
||||
searchClearIcon.visibleProperty().unbind();
|
||||
searchClearIcon.managedProperty().unbind();
|
||||
searchClearIcon.setOnMouseClicked(null);
|
||||
librarySelector.visibleProperty().unbind();
|
||||
librarySelector.managedProperty().unbind();
|
||||
librarySelector.setTooltip(null);
|
||||
|
||||
@@ -59,6 +59,23 @@
|
||||
-fx-padding: 0 10 0 4;
|
||||
}
|
||||
|
||||
/* Search bar's magnifier/clear glyphs — muted so neither outweighs the typed query itself. */
|
||||
.app-header-bar .search-icon {
|
||||
-fx-icon-color: -color-fg-muted;
|
||||
-fx-icon-size: 20px;
|
||||
-fx-padding: 0 0 0 6;
|
||||
}
|
||||
|
||||
.app-header-bar .search-clear-icon {
|
||||
-fx-icon-color: -color-fg-muted;
|
||||
-fx-icon-size: 18px;
|
||||
-fx-padding: 0 6 0 0;
|
||||
}
|
||||
|
||||
.app-header-bar .search-clear-icon:hover {
|
||||
-fx-icon-color: -color-fg-default;
|
||||
}
|
||||
|
||||
/*
|
||||
* The open-library selector: reads as text-in-the-header, not a boxed control. A MenuButton fakes its
|
||||
* border the same way ComboBoxBase does, painting a second background layer under a 1px inset (see
|
||||
|
||||
Reference in New Issue
Block a user