Skip to content

Commit

Permalink
Merge pull request #171 from dlsc-software-consulting-gmbh/enhancment…
Browse files Browse the repository at this point in the history
…-SearchTextField

Enhanced SearchTextField to support search history
  • Loading branch information
dlemmermann authored May 10, 2024
2 parents 681aaa0 + c05eea3 commit 7b38475
Show file tree
Hide file tree
Showing 10 changed files with 882 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.materialdesign.MaterialDesign;

import java.util.ArrayList;
import java.util.Comparator;
Expand All @@ -35,9 +38,11 @@ public void start(Stage primaryStage) throws Exception {
field.getEditor().setPrefColumnCount(30);
field.setOnCommit(country -> System.out.println("on commit listener in demo was invoked, country = " + country));

Region regionLeft = new Region();
FontIcon fontIcon = new FontIcon(MaterialDesign.MDI_HISTORY);
StackPane regionLeft = new StackPane(fontIcon);
regionLeft.setPrefWidth(30);
regionLeft.setStyle("-fx-background-color: red;");
field.setLeft(regionLeft);
// regionLeft.setStyle("-fx-background-color: red;");

Region regionRight = new Region();
regionRight.setPrefWidth(30);
Expand Down Expand Up @@ -80,8 +85,8 @@ public void start(Stage primaryStage) throws Exception {
CheckBox autoCommitOnFocusLostBox = new CheckBox("Auto commit on field lost focus.");
autoCommitOnFocusLostBox.selectedProperty().bindBidirectional(field.autoCommitOnFocusLostProperty());

field.leftProperty().bind(Bindings.createObjectBinding(() -> showLeftRightNodes.isSelected() ? regionLeft : null, showLeftRightNodes.selectedProperty()));
field.rightProperty().bind(Bindings.createObjectBinding(() -> showLeftRightNodes.isSelected() ? regionRight : null, showLeftRightNodes.selectedProperty()));
// field.leftProperty().bind(Bindings.createObjectBinding(() -> showLeftRightNodes.isSelected() ? regionLeft : null, showLeftRightNodes.selectedProperty()));
// field.rightProperty().bind(Bindings.createObjectBinding(() -> showLeftRightNodes.isSelected() ? regionRight : null, showLeftRightNodes.selectedProperty()));

VBox vbox = new VBox(20, createNewItemBox, showPromptText, usePlaceholder, hideWithSingleChoiceBox, hideWithNoChoiceBox, showSearchIconBox, showLeftRightNodes, autoCommitOnFocusLostBox, hBox, hBox2, field);
vbox.setPadding(new Insets(20));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,81 @@
package com.dlsc.gemsfx.demo;

import com.dlsc.gemsfx.RemovableListCell;
import com.dlsc.gemsfx.SearchTextField;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.Spinner;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.time.LocalTime;
import java.util.List;

public class SearchTextFieldApp extends Application {

private SearchTextField field1;

@Override
public void start(Stage primaryStage) throws Exception {

SearchTextField field1 = new SearchTextField();
field1 = new SearchTextField();
field1.setCellFactory(param -> new RemovableListCell<>((listView, item) -> field1.removeHistory(item)));

SearchTextField field2 = new SearchTextField(true);

VBox vbox = new VBox(20, new Label("Standard"), field1, new Label("Round"), field2);
Label label = new Label("Max History Size:");
Spinner<Integer> maxHistorySizeSpinner = new Spinner<>(5, 50, 10, 5);
field1.maxHistorySizeProperty().bind(maxHistorySizeSpinner.valueProperty());
maxHistorySizeSpinner.setMaxWidth(Double.MAX_VALUE);
HBox maxHistorySizeBox = new HBox(5, label, maxHistorySizeSpinner);
maxHistorySizeBox.setAlignment(Pos.CENTER_LEFT);

CheckBox enableHistoryPopupBox = new CheckBox("Enable History Popup");
enableHistoryPopupBox.setSelected(true);
field1.enableHistoryPopupProperty().bindBidirectional(enableHistoryPopupBox.selectedProperty());
field2.enableHistoryPopupProperty().bindBidirectional(enableHistoryPopupBox.selectedProperty());

CheckBox addHistoryOnActionBox = new CheckBox("Add History on Enter");
addHistoryOnActionBox.setSelected(true);
field1.addHistoryOnEnterProperty().bind(addHistoryOnActionBox.selectedProperty());
field2.addHistoryOnEnterProperty().bind(addHistoryOnActionBox.selectedProperty());

Button setHistoryButton = new Button("Set History");
setHistoryButton.setMaxWidth(Double.MAX_VALUE);
setHistoryButton.setOnAction(e -> {
List<String> list = List.of("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten");
field1.setHistory(list);
field2.setHistory(list);
});

Button addHistoryButton = new Button("Add History");
addHistoryButton.setMaxWidth(Double.MAX_VALUE);
addHistoryButton.setOnAction(e -> {
field1.addHistory("New " + LocalTime.now());
field2.addHistory("New" + LocalTime.now());
});

Button removeStandardHistoryButton = createRemoveHistoryButton("Standard Field Remove First History Item", field1);
Button removeRoundHistoryButton = createRemoveHistoryButton("Round Field Remove First History Item", field2);

Button clearButton = new Button("Clear History");
clearButton.setMaxWidth(Double.MAX_VALUE);
clearButton.setOnAction(e -> {
field1.clearHistory();
field2.clearHistory();
});

VBox vbox = new VBox(20, new Label("Standard"), field1, new Label("Round"), field2,
new Separator(), maxHistorySizeBox, enableHistoryPopupBox, addHistoryOnActionBox,
setHistoryButton, addHistoryButton, removeStandardHistoryButton, removeRoundHistoryButton, clearButton);
vbox.setPadding(new Insets(20));

Scene scene = new Scene(vbox);
Expand All @@ -27,6 +86,14 @@ public void start(Stage primaryStage) throws Exception {
primaryStage.show();
}

private Button createRemoveHistoryButton(String text, SearchTextField field) {
Button removeHistoryButton2 = new Button(text);
removeHistoryButton2.disableProperty().bind(Bindings.createObjectBinding(() -> field.getUnmodifiableHistory().isEmpty(), field.getUnmodifiableHistory()));
removeHistoryButton2.setMaxWidth(Double.MAX_VALUE);
removeHistoryButton2.setOnAction(e -> field.removeHistory(field.getUnmodifiableHistory().get(0)));
return removeHistoryButton2;
}

public static void main(String[] args) {
launch(args);
}
Expand Down
52 changes: 52 additions & 0 deletions gemsfx/src/main/java/com/dlsc/gemsfx/CustomPopupControl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.dlsc.gemsfx;

import javafx.css.PseudoClass;
import javafx.geometry.NodeOrientation;
import javafx.scene.Node;
import javafx.scene.control.PopupControl;
import javafx.stage.Screen;
import javafx.stage.Window;

/**
* A custom popup control that extends PopupControl.
* <p>
* The popup can be displayed above or below the anchor node depending on the available space.
*/
public class CustomPopupControl extends PopupControl {

private static final PseudoClass ABOVE_PSEUDO_CLASS = PseudoClass.getPseudoClass("above");
private static final PseudoClass BELOW_PSEUDO_CLASS = PseudoClass.getPseudoClass("below");

public void show(Node node) {
if (node.getScene() != null && node.getScene().getWindow() != null) {
Window parent = node.getScene().getWindow();
getScene().setNodeOrientation(node.getEffectiveNodeOrientation());
if (node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
setAnchorLocation(AnchorLocation.CONTENT_TOP_RIGHT);
} else {
setAnchorLocation(AnchorLocation.CONTENT_TOP_LEFT);
}

double nodeTopY = parent.getY() + node.localToScene(0.0D, 0.0D).getY() + node.getScene().getY();

double anchorX = parent.getX() + node.localToScene(0.0D, 0.0D).getX() + node.getScene().getX();
double anchorY = nodeTopY + node.getBoundsInParent().getHeight();

double bridgeHeight = bridge.getHeight();
double popupHeight = bridgeHeight == 0 ? getSkin().getNode().prefHeight(-1) : bridgeHeight;
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();

boolean isShowAbove = anchorY + popupHeight > screenHeight;
if (isShowAbove) {
anchorY = nodeTopY - popupHeight;
}
this.pseudoClassStateChanged(ABOVE_PSEUDO_CLASS, isShowAbove);
this.pseudoClassStateChanged(BELOW_PSEUDO_CLASS, !isShowAbove);

show(node, anchorX, anchorY);
} else {
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
}
}

}
106 changes: 106 additions & 0 deletions gemsfx/src/main/java/com/dlsc/gemsfx/RemovableListCell.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.dlsc.gemsfx;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.materialdesign.MaterialDesign;

import java.util.Objects;
import java.util.function.BiConsumer;

/**
* A list cell that displays a remove button on the right side. The remove button is only
* visible when the mouse hovers over the cell. When the remove button is clicked, the
* onRemove callback is invoked.
*
* @param <T> the type of the list cell item
*/
public class RemovableListCell<T> extends ListCell<T> {

private final HBox containerBox;
private final Label label;

public RemovableListCell() {
getStyleClass().add("removable-list-cell");

label = new Label();

StackPane removeBtn = new StackPane(new FontIcon(MaterialDesign.MDI_CLOSE));
removeBtn.getStyleClass().add("remove-button");
removeBtn.setOnMouseClicked(this::onRemoveAction);

containerBox = new HBox(label, new Spacer(), removeBtn);
containerBox.getStyleClass().add("container-box");
containerBox.setAlignment(Pos.CENTER_LEFT);
}

public RemovableListCell(BiConsumer<ListView<T>, T> onRemove) {
this();
setOnRemove(onRemove);
}

@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);

if (item == null || empty) {
label.setText(null);

setText(null);
setGraphic(null);
} else {
label.setText(item.toString());

setText(null);
setGraphic(containerBox);
}
}

public void onRemoveAction(MouseEvent event) {
if (getOnRemove() != null) {

// clear selection if the item is selected
if (isSelected()) {
getListView().getSelectionModel().clearSelection();
}

getOnRemove().accept(getListView(), getItem());
}
}

private ObjectProperty<BiConsumer<ListView<T>, T>> onRemove;

/**
* A callback that is invoked when the remove button is clicked.
*
* @return the onRemoveProperty
*/
public final ObjectProperty<BiConsumer<ListView<T>, T>> onRemoveProperty() {
if (onRemove == null) {
onRemove = new SimpleObjectProperty<>(this, "onRemove");
}
return onRemove;
}

public final BiConsumer<ListView<T>, T> getOnRemove() {
return onRemove == null ? null : onRemoveProperty().get();
}

public final void setOnRemove(BiConsumer<ListView<T>, T> onRemove) {
onRemoveProperty().set(onRemove);
}

@Override
public String getUserAgentStylesheet() {
return Objects.requireNonNull(RemovableListCell.class.getResource("removable-list-cell.css")).toExternalForm();
}
}


Loading

0 comments on commit 7b38475

Please sign in to comment.