Reputation: 10542
Hey all I need some assistance in getting my tag app working as it should. Currently it displays values like this:
However, I am wanting it to wrap the tags instead of continuing to go horizontal. I changed all the HBox's to FlowPane's but that made it look all weird and didn't work well. I also tried doing VBox's but it still messes it up.
What I am wanting it to look like is this:
The current code:
package application;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.stage.Stage;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class Main3 extends Application{
Color txtColor;
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("TagInputFx Demo");
VBox root = new VBox();
AutocompleteMultiSelectionBox tagInput = new AutocompleteMultiSelectionBox();
ObservableSet<String> sug = FXCollections.observableSet();
String[] listOfDBCalls = "Integrated Security=, user=, password=, pwd=, Database=, Encrypt=, Trusted_Connection=, Persist Security Info=, TrustServerCertificate=, User ID=, Initial Catalog=, AttachDbFileName=, Failover Partner=, Asynchronous Processing=, User Instance=, Packet Size=, Column Encryption Setting=, Network Library=, MultipleActiveResultSets=, Data Source=, Server=, Enclave Attestation Url=, Provider=, UID=, Connect Timeout=, Driver=, MARS_Connection=, Proxy Password=, Proxy User Id=, Host=, Pooling=, Max Pool Size=, Connection Lifetime=, Incr Pool Size=, Decr Pool Size=, DBA Privilege=, Load Balancing=, Dbq=, DistribTX=, OledbKey1=, OledbKey2=, ConnectString=, Version=, New=, UseUTF16Encoding=, Legacy Format=, Read Only=, DateTimeFormat=, BinaryGUID=, Cache Size=, Page Size=, Enlist=, Max Page Count=, Journal Mode=, Synchronous=, Compress=, UTF8Encoding=, Timeout=, NoTXN=, SyncPragma=, StepAPI=, LongNames=, Port=, location=, sslmode=, Protocol=, SslMode=, MinPoolSize=, MaxPoolSize=".split(",");
for (String call : listOfDBCalls) {
sug.add(call);
}
tagInput.setSuggestions(sug);
tagInput.setTextColor(Color.BLACK);
Text header = new Text("Tags:");
root.getChildren().addAll(header, tagInput);
Scene scene = new Scene(root, 600, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main3(String[] args) {
launch(args);
}
public class AutocompleteMultiSelectionBox extends HBox {
private final ObservableList<String> tags;
private final ObservableSet<String> suggestions;
private ContextMenu entriesPopup;
private static final int MAX_ENTRIES = 15;
private final TextField inputTextField;
public AutocompleteMultiSelectionBox() {
getStyleClass().setAll("tag-bar");
getStylesheets().add(getClass().getResource("application.css").toExternalForm());
tags = FXCollections.observableArrayList();
suggestions = FXCollections.observableSet();
inputTextField = new TextField();
this.entriesPopup = new ContextMenu();
setListner();
inputTextField.setOnKeyPressed(event -> {
if (event.getCode().equals(KeyCode.BACK_SPACE) && !tags.isEmpty() && inputTextField.getText().isEmpty()) {
String last = tags.get(tags.size() - 1);
String orgTag = last.split("=")[0] + "=";
if (orgTag.length() > 2) {
suggestions.add(orgTag);
tags.remove(last);
}
} else if (event.getCode().toString() == "ENTER" || event.getCode().toString() == "TAB" && !inputTextField.getText().isEmpty()) {
String newTag = inputTextField.getText();
String orgTag = newTag.split("=")[0] + "=";
if (orgTag.length() > 2) {
suggestions.add(newTag);
tags.add(newTag);
inputTextField.setText("");
suggestions.remove(orgTag);
suggestions.remove(newTag);
}
} else if (event.getCode().toString() == "ESCAPE") {
inputTextField.clear();
} else if (event.getCode().toString() == "DOWN" || event.getCode().toString() == "UP") {
entriesPopup.getSkin().getNode().lookup(".menu-item").requestFocus();
}
System.out.println(event.getCode().toString());
});
inputTextField.prefHeightProperty().bind(this.heightProperty());
HBox.setHgrow(inputTextField, Priority.ALWAYS);
inputTextField.setBackground(null);
tags.addListener((ListChangeListener.Change<? extends String> change) -> {
while (change.next()) {
if (change.wasPermutated()) {
ArrayList<Node> newSublist = new ArrayList<>(change.getTo() - change.getFrom());
for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
newSublist.add(null);
}
for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
newSublist.set(change.getPermutation(i), getChildren().get(i));
}
getChildren().subList(change.getFrom(), change.getTo()).clear();
getChildren().addAll(change.getFrom(), newSublist);
} else {
if (change.wasRemoved()) {
getChildren().subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
}
if (change.wasAdded()) {
getChildren().addAll(change.getFrom(), change.getAddedSubList().stream().map(Tag::new).collect(
Collectors.toList()));
}
}
}
});
getChildren().add(inputTextField);
}
private TextFlow buildTextFlow(String text, String filter) {
int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase());
Text textBefore = new Text(text.substring(0, filterIndex));
Text textAfter = new Text(text.substring(filterIndex + filter.length()));
Text textFilter = new Text(text.substring(filterIndex, filterIndex + filter.length()));
textFilter.setFill(Color.ORANGE);
textFilter.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));
return new TextFlow(textBefore, textFilter, textAfter);
}
private void setListner() {
inputTextField.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.isEmpty()) {
entriesPopup.hide();
} else {
List<String> filteredEntries = suggestions
.stream()
.filter(e -> e.toLowerCase().contains(newValue.toLowerCase()))
.collect(Collectors.toList());
if (!filteredEntries.isEmpty()) {
populatePopup(filteredEntries, newValue);
if (!entriesPopup.isShowing()) {
entriesPopup.show(this, Side.BOTTOM, 0, 0);
}
} else {
entriesPopup.hide();
}
}
});
focusedProperty().addListener((observableValue, oldValue, newValue) -> entriesPopup.hide());
}
private void populatePopup(List<String> searchResult, String searchRequest) {
List<CustomMenuItem> menuItems = new LinkedList<>();
searchResult.stream()
.limit(MAX_ENTRIES)
.forEach(result -> {
TextFlow textFlow = buildTextFlow(result, searchRequest);
textFlow.prefWidthProperty().bind(AutocompleteMultiSelectionBox.this.widthProperty());
CustomMenuItem item = new CustomMenuItem(textFlow, true);
menuItems.add(item);
item.setOnAction(actionEvent -> {
if (result.endsWith("=")) {
//Dont close it yet.. keep typing
inputTextField.setText(result);
inputTextField.requestFocus();
inputTextField.end();
} else {
//It has values on both sides of the =
//so lets add it to the tag list
tags.add(result);
suggestions.remove(result);
inputTextField.clear();
entriesPopup.hide();
}
});
});
entriesPopup.getItems().clear();
entriesPopup.getItems().addAll(menuItems);
}
public final ObservableList<String> getTags() {
return tags;
}
public final ObservableSet<String> getSuggestions() {
return suggestions;
}
public final void setTextColor(Color c) {
txtColor = c;
}
public final void setSuggestions(ObservableSet<String> suggestions) {
this.suggestions.clear();
this.suggestions.addAll(suggestions);
}
private class Tag extends HBox {
Tag(String tag) {
Button removeButton = new Button();
Image img = new Image(getClass().getResource("delete.png").toExternalForm());
ImageView view = new ImageView(img);
view.setFitHeight(16);
view.setPreserveRatio(true);
getStyleClass().add("tag");
removeButton.setBackground(null);
removeButton.setOnAction(event -> {
tags.remove(tag);
suggestions.add(tag);
inputTextField.requestFocus();
});
Text text = new Text(tag);
text.setFill(txtColor);
text.setFont(Font.font(text.getFont().getFamily(), FontWeight.BOLD, text.getFont().getSize()));
setAlignment(Pos.CENTER);
setSpacing(2);
setPadding(new Insets(0, 0, 0, 5));
removeButton.setGraphic(view);
getChildren().addAll(text, removeButton);
}
}
}
}
Is there a way of doing this with the hbox?
UPDATE 1
Using the code for the FlowPane this is what the output looks like:
The modified code to replace HBox with FlowPane:
package application;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.stage.Stage;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class Main3 extends Application{
Color txtColor;
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("TagInputFx Demo");
VBox root = new VBox();
AutocompleteMultiSelectionBox tagInput = new AutocompleteMultiSelectionBox();
ObservableSet<String> sug = FXCollections.observableSet();
String[] listOfDBCalls = "Integrated Security=, user=, password=, pwd=, Database=, Encrypt=, Trusted_Connection=, Persist Security Info=, TrustServerCertificate=, User ID=, Initial Catalog=, AttachDbFileName=, Failover Partner=, Asynchronous Processing=, User Instance=, Packet Size=, Column Encryption Setting=, Network Library=, MultipleActiveResultSets=, Data Source=, Server=, Enclave Attestation Url=, Provider=, UID=, Connect Timeout=, Driver=, MARS_Connection=, Proxy Password=, Proxy User Id=, Host=, Pooling=, Max Pool Size=, Connection Lifetime=, Incr Pool Size=, Decr Pool Size=, DBA Privilege=, Load Balancing=, Dbq=, DistribTX=, OledbKey1=, OledbKey2=, ConnectString=, Version=, New=, UseUTF16Encoding=, Legacy Format=, Read Only=, DateTimeFormat=, BinaryGUID=, Cache Size=, Page Size=, Enlist=, Max Page Count=, Journal Mode=, Synchronous=, Compress=, UTF8Encoding=, Timeout=, NoTXN=, SyncPragma=, StepAPI=, LongNames=, Port=, location=, sslmode=, Protocol=, SslMode=, MinPoolSize=, MaxPoolSize=".split(",");
for (String call : listOfDBCalls) {
sug.add(call);
}
tagInput.setSuggestions(sug);
tagInput.setTextColor(Color.BLACK);
Text header = new Text("Tags:");
root.getChildren().addAll(header, tagInput);
Scene scene = new Scene(root, 600, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main3(String[] args) {
launch(args);
}
public class AutocompleteMultiSelectionBox extends FlowPane {
private final ObservableList<String> tags;
private final ObservableSet<String> suggestions;
private ContextMenu entriesPopup;
private static final int MAX_ENTRIES = 15;
private final TextField inputTextField;
public AutocompleteMultiSelectionBox() {
getStyleClass().setAll("tag-bar");
getStylesheets().add(getClass().getResource("application.css").toExternalForm());
tags = FXCollections.observableArrayList();
suggestions = FXCollections.observableSet();
inputTextField = new TextField();
this.entriesPopup = new ContextMenu();
setListner();
inputTextField.setOnKeyPressed(event -> {
if (event.getCode().equals(KeyCode.BACK_SPACE) && !tags.isEmpty() && inputTextField.getText().isEmpty()) {
String last = tags.get(tags.size() - 1);
String orgTag = last.split("=")[0] + "=";
if (orgTag.length() > 2) {
suggestions.add(orgTag);
tags.remove(last);
}
} else if (event.getCode().toString() == "ENTER" || event.getCode().toString() == "TAB" && !inputTextField.getText().isEmpty()) {
String newTag = inputTextField.getText();
String orgTag = newTag.split("=")[0] + "=";
if (orgTag.length() > 2) {
suggestions.add(newTag);
tags.add(newTag);
inputTextField.setText("");
suggestions.remove(orgTag);
suggestions.remove(newTag);
}
} else if (event.getCode().toString() == "ESCAPE") {
inputTextField.clear();
} else if (event.getCode().toString() == "DOWN" || event.getCode().toString() == "UP") {
entriesPopup.getSkin().getNode().lookup(".menu-item").requestFocus();
}
System.out.println(event.getCode().toString());
//inputTextField.clear();
});
inputTextField.prefHeightProperty().bind(this.heightProperty());
//HBox.setHgrow(inputTextField, Priority.ALWAYS);
inputTextField.setBackground(null);
tags.addListener((ListChangeListener.Change<? extends String> change) -> {
while (change.next()) {
if (change.wasPermutated()) {
ArrayList<Node> newSublist = new ArrayList<>(change.getTo() - change.getFrom());
for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
newSublist.add(null);
}
for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
newSublist.set(change.getPermutation(i), getChildren().get(i));
}
getChildren().subList(change.getFrom(), change.getTo()).clear();
getChildren().addAll(change.getFrom(), newSublist);
} else {
if (change.wasRemoved()) {
getChildren().subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
}
if (change.wasAdded()) {
getChildren().addAll(change.getFrom(), change.getAddedSubList().stream().map(Tag::new).collect(
Collectors.toList()));
}
}
}
});
getChildren().add(inputTextField);
}
private TextFlow buildTextFlow(String text, String filter) {
int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase());
Text textBefore = new Text(text.substring(0, filterIndex));
Text textAfter = new Text(text.substring(filterIndex + filter.length()));
Text textFilter = new Text(text.substring(filterIndex, filterIndex + filter.length()));
textFilter.setFill(Color.ORANGE);
textFilter.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));
return new TextFlow(textBefore, textFilter, textAfter);
}
private void setListner() {
inputTextField.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.isEmpty()) {
entriesPopup.hide();
} else {
List<String> filteredEntries = suggestions
.stream()
.filter(e -> e.toLowerCase().contains(newValue.toLowerCase()))
.collect(Collectors.toList());
if (!filteredEntries.isEmpty()) {
populatePopup(filteredEntries, newValue);
if (!entriesPopup.isShowing()) {
entriesPopup.show(this, Side.BOTTOM, 0, 0);
}
} else {
entriesPopup.hide();
}
}
});
focusedProperty().addListener((observableValue, oldValue, newValue) -> entriesPopup.hide());
}
private void populatePopup(List<String> searchResult, String searchRequest) {
List<CustomMenuItem> menuItems = new LinkedList<>();
searchResult.stream()
.limit(MAX_ENTRIES)
.forEach(result -> {
TextFlow textFlow = buildTextFlow(result, searchRequest);
textFlow.prefWidthProperty().bind(AutocompleteMultiSelectionBox.this.widthProperty());
CustomMenuItem item = new CustomMenuItem(textFlow, true);
menuItems.add(item);
item.setOnAction(actionEvent -> {
if (result.endsWith("=")) {
//Dont close it yet.. keep typing
inputTextField.setText(result);
inputTextField.requestFocus();
inputTextField.end();
} else {
//It has values on both sides of the =
//so lets add it to the tag list
tags.add(result);
suggestions.remove(result);
inputTextField.clear();
entriesPopup.hide();
}
});
});
entriesPopup.getItems().clear();
entriesPopup.getItems().addAll(menuItems);
}
public final ObservableList<String> getTags() {
return tags;
}
public final ObservableSet<String> getSuggestions() {
return suggestions;
}
public final void setTextColor(Color c) {
txtColor = c;
}
public final void setSuggestions(ObservableSet<String> suggestions) {
this.suggestions.clear();
this.suggestions.addAll(suggestions);
}
private class Tag extends FlowPane {
Tag(String tag) {
Button removeButton = new Button();
Image img = new Image(getClass().getResource("delete.png").toExternalForm());
ImageView view = new ImageView(img);
view.setFitHeight(16);
view.setPreserveRatio(true);
getStyleClass().add("tag");
removeButton.setBackground(null);
removeButton.setOnAction(event -> {
tags.remove(tag);
suggestions.add(tag);
inputTextField.requestFocus();
});
Text text = new Text(tag);
text.setFill(txtColor);
text.setFont(Font.font(text.getFont().getFamily(), FontWeight.BOLD, text.getFont().getSize()));
setAlignment(Pos.CENTER);
//setSpacing(2);
setPadding(new Insets(0, 0, 0, 5));
removeButton.setGraphic(view);
getChildren().addAll(text, removeButton);
}
}
}
}
UPDATE 2
Here is the CSS (application.css):
/* JavaFX CSS - Leave this comment until you have at least create one rule which uses -fx-Property */
.tag-bar {
-fx-border-color: #dbdae7;
-fx-spacing: 1;
-fx-padding: 1;
-fx-max-height: 30;
}
.tag-bar .tag {
-fx-border-color: b7b5cf;
-fx-background-color: #dbdae7;
-fx-border-radius: 10;
-fx-background-radius: 10;
}
.tag-bar .tag {
-fx-text-fill: #000;
}
.tag-bar #tagText {
-fx-text-fill: orange;
-fx-font-weight: bold;
}
And the png (delete.png):
Upvotes: 1
Views: 109
Reputation: 10542
It seems that all that was needed to do was the following:
Removing the line:
inputTextField.prefHeightProperty().bind(this.heightProperty());
Editing the line:
public class AutocompleteMultiSelectionBox extends VBox {
to
public class AutocompleteMultiSelectionBox extends FlowPane {
Upvotes: 0
Reputation: 209225
TLDR: Make the tag container a FlowPane
and each individual tag a Label
.
An HBox
doesn't wrap: from the documentation:
HBox
lays out its children in a single horizontal row
(my emphasis).
To wrap, you need a FlowPane
instead of the HBox
.
There is no need to represent each individual tag with a FlowPane
, however: if you do so the layout for the tag will be incorrect (for example, the preferred width of a flow pane will default to 400 in this case; again see the documentation).
In fact, as pointed out in the comments, you can fairly easily just represent each tag with just a Label
, which will avoid the unnecessary subclassing. (Generally, your UI code should follow all the usual good coding practices, such as "prefer aggregation to inheritance".) Using managed nodes, such as Control
and Region
and their subclasses, will generally work better for layout than using unmanaged nodes such as Shape
and its subclasses (including Text
).
Here is a pared-down version of what I think you are trying to do, with most of the functionality that is not related to layout problems, along with any dependence on external resources, removed. I provided just a little styling of the tags but this can obviously be improved.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class TagDemo extends Application {
private ObservableList<String> tags;
@Override
public void start(Stage stage) {
tags = FXCollections.observableArrayList();
FlowPane tagPane = new FlowPane();
tagPane.setHgap(5);
tagPane.setVgap(5);
tags.addListener((ListChangeListener.Change<? extends String> change) -> {
while (change.next()) {
if (change.wasRemoved()) {
int from = change.getFrom();
int to = change.getTo();
tagPane.getChildren().subList(from, to + 1).clear();
}
if (change.wasAdded()) {
tagPane.getChildren().addAll(
change.getFrom(),
change.getAddedSubList().stream().map(this::createTag).toList()
);
}
}
});
TextField tagField = new TextField();
tagField.setPromptText("New Tag");
tagField.setOnAction(_ -> {
if (! tagField.getText().isBlank()) {
tags.add(tagField.getText());
tagField.clear();
}
});
tagPane.getChildren().add(tagField);
BorderPane root = new BorderPane(tagPane);
root.setTop(new Label("Tags:"));
Scene scene = new Scene(root, 800, 500);
stage.setScene(scene);
stage.show();
}
private Node createTag(String tagName) {
Label label = new Label(tagName);
Button delete = new Button("X");
delete.setOnAction(_ -> tags.remove(tagName));
label.setGraphic(delete);
label.setContentDisplay(ContentDisplay.RIGHT);
label.setGraphicTextGap(1);
label.setStyle("-fx-color: lightsteelblue; -fx-background-color: lightsteelblue;");
return label;
}
public static void main(String[] args) {
launch();
}
}
Upvotes: 3