So I am writing an AutosuggestMenu that adds a listener to a TextField and recommends suggestions in a popup ContextMenu, based on comparing the keystrokes entered with the Collection of words provided.
Unfortunately, changes to the ContextMenu elements are not displayed, and I suspect this is because I am modifying the elements of the ObservableList associated with the ContextMenu and not the list itself.
Browsing stack has led to believe I should implement an extractor, but based on the examples provided I have no idea how to do this for my specific problem. Any solution would be very much appreciated!
Source:
package com.sknb.gui;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javafx.geometry.Side;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
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 AutosuggestMenu {
public final static int DEFAULT_MENU_SIZE = 10;
//Data members
private int menuSize;
private Collection<String> wordList;
//GUI members
private ContextMenu menu;
private final Text textBefore, textMatching, textAfter;
public AutosuggestMenu(Collection<String> keyList) {
this(keyList, DEFAULT_MENU_SIZE);
}
public AutosuggestMenu(Collection<String> keyList, int numEntries) {
if (keyList == null) {
throw new NullPointerException();
}
this.wordList = keyList;
this.menuSize = numEntries;
this.menu = new ContextMenu();
for (int i = 0; i < this.menuSize; i++) {
CustomMenuItem item = new CustomMenuItem(new Label(), true);
this.menu.getItems().add(item);
}
this.textBefore = new Text();
this.textMatching = new Text();
this.textAfter = new Text();
}
public void addListener(TextField field) {
field.textProperty().addListener((observable, oldValue, newValue) -> {
String enteredText = field.getText();
if (enteredText == null || enteredText.isEmpty()) {
this.menu.hide();
} else {
List<String> filteredEntries = this.wordList.stream()
.filter(e -> e.contains(enteredText))
.collect(Collectors.toList());
if (!filteredEntries.isEmpty()) {
populatePopup(field, filteredEntries, enteredText);
if (!(this.menu.isShowing())) {
this.menu.show(field, Side.BOTTOM, 0, 0);
}
} else {
this.menu.hide();
}
}
});
field.focusedProperty().addListener((observableValue, oldValue, newValue) -> {
this.menu.hide();
});
}
private void populatePopup(TextField field, List<String> matches, String query) {
int i = 0,
max = (matches.size() > this.menuSize) ? this.menuSize :
matches.size();
for (MenuItem item : this.menu.getItems()) {
if (i < max) {
String result = matches.get(i);
item.setGraphic(generateTextFlow(result, query));
item.setVisible(true);
item.setOnAction(actionEvent -> {
field.setText(result);
field.positionCaret(result.length());
this.menu.hide();
});
} else {
item.setVisible(false);
}
i++;
}
}
private TextFlow generateTextFlow(String text, String filter) {
int filterIndex = text.indexOf(filter);
this.textBefore.setText(text.substring(0, filterIndex));
this.textAfter.setText(text.substring(filterIndex + filter.length()));
this.textMatching.setText(text.substring(filterIndex, filterIndex + filter.length()));
textMatching.setFill(Color.BLUE);
textMatching.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));
return new TextFlow(textBefore, textMatching, textAfter);
}
public int getMenuSize() {
return this.menuSize;
}
public void setMenuSize(int size) {
this.menuSize = size;
}
public Collection<String> getKeyList() {
return this.wordList;
}
public void setKeyList(Collection<String> keyList) {
this.wordList = keyList;
}
//To do: add ways to change style of ContextMenu/menu items/text elements
}
Test class using a text file of English words as a dictionary:
package autosuggestfieldtest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import com.sknb.gui.AutosuggestMenu;
public class AutosuggestFieldTest extends Application {
private TextField textfield;
private Collection<String> words;
private AutosuggestMenu popup;
#Override
public void start(Stage primaryStage) {
String filename = Paths.get("").toAbsolutePath().toString() + "\\words.txt";
try (Stream<String> stream = Files.lines(Paths.get(filename))) {
words = stream
.collect(Collectors.toSet());
} catch (IOException e) {
System.out.println("DERP");
}
popup = new AutosuggestMenu(words);
textfield = new TextField();
popup.addListener(textfield);
StackPane root = new StackPane();
root.getChildren().add(textfield);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("AutocompleteField Test");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Note: this is a modification of a similar solution by Ruslan, but I was wondering if there is a solution that does not involve clearing/repopulating the menu with every keystroke? I.e. just using the setGraphic and refreshing the ContextMenu?
Related
I am making a to do list with java FXML using file reader and writer and scene builder. I have been trying to add a completed task function but tried many ways and am unable to figure out something which will work for me. I tried making a separate list where I keep my completed task but its still not working out for me. At this moment I have tried a lot by myself and am willing to go for another for another method to add this function. I am using To Do Date Class which is using to do Item class.
I have tried using it as predicate of filtered list and type casting accordingly but it hasn't worked out for me either.Rest of code works perfectly fine.
controller is:
package todolist;
import java.io.IOException;
import java.net.URL;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TextArea;
import javafx.scene.control.ToggleButton;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.util.Callback;
/**
*
* #author Pc Planet
*/
public class FXMLDocumentController {
private List<ToDoItem> todoItems;
#FXML
private ListView<ToDoItem> todoListView;
#FXML
private TextArea itemDetailsTextArea;
#FXML
private Label deadlineLabel;
#FXML
private BorderPane mainBorderPane;
#FXML
private ContextMenu listContextMenu;
#FXML
private ToggleButton filterToggleButton;
#FXML
private Button exit,ADD;
private FilteredList<ToDoItem> filteredList;
private Predicate<ToDoItem> wantAllItems;
private Predicate<ToDoItem> wantTodaysItems;
private Predicate<ToDoItem> uncompleted;
private ObservableList<ToDoItem> completed;
public void initialize() {
// delete
listContextMenu = new ContextMenu();
MenuItem deleteMenuItem = new MenuItem("Delete");
deleteMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
ToDoItem item = todoListView.getSelectionModel().getSelectedItem();
deleteItem(item);
}
});
// Completed
listContextMenu = new ContextMenu();
MenuItem completedMenuItem = new MenuItem("Mark as complete");
completedMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
ToDoItem item = todoListView.getSelectionModel().getSelectedItem();
completedItem(item);
}
});
listContextMenu.getItems().addAll(deleteMenuItem);
listContextMenu.getItems().addAll(completedMenuItem);
//change
todoListView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<ToDoItem>() {
#Override
public void changed(ObservableValue<? extends ToDoItem> observable, ToDoItem oldValue, ToDoItem newValue) {
if(newValue != null) {
ToDoItem item = todoListView.getSelectionModel().getSelectedItem();
itemDetailsTextArea.setText(item.getDetails());
DateTimeFormatter df = DateTimeFormatter.ofPattern("MMMM d, yyyy"); // "d M yy");
deadlineLabel.setText(df.format(item.getDeadline()));
}
}
});
wantAllItems = (ToDoItem todoItem) -> true;
wantTodaysItems = (ToDoItem todoItem) -> (todoItem.getDeadline().equals(LocalDate.now()));
filteredList = new FilteredList<ToDoItem>(ToDoData.getInstance().getTodoItems(), wantAllItems);
// list sort
SortedList<ToDoItem> sortedList = new SortedList<ToDoItem>(filteredList,
new Comparator<ToDoItem>() {
#Override
public int compare(ToDoItem o1, ToDoItem o2) {
return o1.getDeadline().compareTo(o2.getDeadline());
}
});
todoListView.setItems(sortedList);
todoListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
todoListView.getSelectionModel().selectFirst();
todoListView.setCellFactory(new Callback<ListView<ToDoItem>, ListCell<ToDoItem>>() {
#Override
public ListCell<ToDoItem> call(ListView<ToDoItem> param) {
ListCell<ToDoItem> cell = new ListCell<ToDoItem>() {
#Override
protected void updateItem(ToDoItem item, boolean empty) {
super.updateItem(item, empty);
if(empty) {
setText(null);
} else {
setText(item.getShortDescription());
if(item.getDeadline().isBefore(LocalDate.now().plusDays(1))) {
setTextFill(Color.RED);
} else {
setTextFill(Color.GREEN);
}
}
}
};
cell.emptyProperty().addListener(
(obs, wasEmpty, isNowEmpty) -> {
if(isNowEmpty) {
cell.setContextMenu(null);
} else {
cell.setContextMenu(listContextMenu);
}
});
return cell;
}
});
}
#FXML
public void showNewItemDialog() {
Dialog<ButtonType> dialog = new Dialog<>();
dialog.initOwner(mainBorderPane.getScene().getWindow());
dialog.setTitle("Add New Todo Item");
dialog.setHeaderText("Use this dialog to create a new todo item");
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(getClass().getResource("todoitemdialog.fxml"));
try {
dialog.getDialogPane().setContent(fxmlLoader.load());
} catch(IOException e) {
System.out.println("Couldn't load the dialog");
e.printStackTrace();
return;
}
dialog.getDialogPane().getButtonTypes().add(ButtonType.OK);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL);
Optional<ButtonType> result = dialog.showAndWait();
if(result.isPresent() && result.get() == ButtonType.OK) {
DialogController controller = fxmlLoader.getController();
ToDoItem newItem = controller.processResults();
todoListView.getSelectionModel().select(newItem);
}
}
#FXML
public void completedtasks(ActionEvent event){
}
#FXML
public void handleKeyPressed(KeyEvent keyEvent) {
ToDoItem selectedItem = todoListView.getSelectionModel().getSelectedItem();
if(selectedItem != null) {
if(keyEvent.getCode().equals(KeyCode.DELETE)) {
deleteItem(selectedItem);}
else{
completedItem(selectedItem);
}
}
}
public void handleClickListView() {
ToDoItem item = todoListView.getSelectionModel().getSelectedItem();
itemDetailsTextArea.setText(item.getDetails());
deadlineLabel.setText(item.getDeadline().toString());
}
public void deleteItem(ToDoItem item) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Delete Todo Item");
alert.setHeaderText("Delete item: " + item.getShortDescription());
alert.setContentText("Are you sure? Press OK to confirm or cancel to Back out.");
Optional<ButtonType> result = alert.showAndWait();
if(result.isPresent() && (result.get() == ButtonType.OK)) {
ToDoData.getInstance().deleteTodoItem(item);
}
}
public void completedItem(ToDoItem item) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Mark as Completed");
alert.setHeaderText("Completed task: " + item.getShortDescription());
alert.setContentText("Are you sure? Press OK to confirm or cancel to Back out.");
Optional<ButtonType> result = alert.showAndWait();
if(result.isPresent() && (result.get() == ButtonType.OK)) {
ToDoData.getInstance().completed(item);
}
}
#FXML
public void wantTodaysItems(){
ToDoItem selectedItem = todoListView.getSelectionModel().getSelectedItem();
filteredList.setPredicate(wantTodaysItems);
if(filteredList.isEmpty()) {
itemDetailsTextArea.clear();
deadlineLabel.setText("");
} else if(filteredList.contains(selectedItem)) {
todoListView.getSelectionModel().select(selectedItem);
} else {
todoListView.getSelectionModel().selectFirst();
}
}
#FXML
public void wantAllItems(){
ToDoItem selectedItem = todoListView.getSelectionModel().getSelectedItem();
filteredList.setPredicate(wantAllItems);
if(filteredList.isEmpty()) {
itemDetailsTextArea.clear();
deadlineLabel.setText("");
} else if(filteredList.contains(selectedItem)) {
todoListView.getSelectionModel().select(selectedItem);
} else {
todoListView.getSelectionModel().selectFirst();
}
}
#FXML
public void handleExit() {
Platform.exit();
}
}
ToDoData class is:
package todolist;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Iterator;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class ToDoData {
private static ToDoData instance = new ToDoData();
private static String filename = "TodoListItems.txt";
private ObservableList<ToDoItem> todoItems;
private ObservableList<ToDoItem> completed;
private DateTimeFormatter formatter;
public static ToDoData getInstance() {
return instance;
}
private ToDoData() {
formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
}
public ObservableList<ToDoItem> getTodoItems() {
return todoItems;
}
public void addTodoItem(ToDoItem item) {
todoItems.add(item);
}
public void loadTodoItems() throws IOException {
todoItems = FXCollections.observableArrayList();
Path path = Paths.get(filename);
BufferedReader br = Files.newBufferedReader(path);
String input;
try {
while ((input = br.readLine()) != null) {
String[] itemPieces = input.split("\t");
String shortDescription = itemPieces[0];
String details = itemPieces[1];
String dateString = itemPieces[2];
LocalDate date = LocalDate.parse(dateString, formatter);
ToDoItem todoItem = new ToDoItem(shortDescription, details, date);
todoItems.add(todoItem);
}
} finally {
if(br != null) {
br.close();
}
}
}
public void storeTodoItems() throws IOException {
Path path = Paths.get(filename);
BufferedWriter bw = Files.newBufferedWriter(path);
try {
Iterator<ToDoItem> iter = todoItems.iterator();
while(iter.hasNext()) {
ToDoItem item = iter.next();
bw.write(String.format("%s\t%s\t%s",
item.getShortDescription(),
item.getDetails(),
item.getDeadline().format(formatter)));
bw.newLine();
}
} finally {
if(bw != null) {
bw.close();
}
}
}
public void deleteTodoItem(ToDoItem item) {
todoItems.remove(item);
}
public void completed(ToDoItem item){
completed.add(item);
}
public ObservableList <ToDoItem> completed(){
return completed;
}
}
todoItem class is:
package todolist;
import java.time.LocalDate;
public class ToDoItem {
private String shortDescription;
private String details;
private LocalDate deadline;
public ToDoItem(String shortDescription, String details, LocalDate deadline) {
this.shortDescription = shortDescription;
this.details = details;
this.deadline = deadline;
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public LocalDate getDeadline() {
return deadline;
}
public void setDeadline(LocalDate deadline) {
this.deadline = deadline;
}
}
This answer is assuming I understand you correctly. In this example, I use a FilteredList on the TableView, and I used the FilteredList's Predicate to show the data in the TableView based on the Button that is pressed. I altered the answer from here.
KeyCode 1
This code allows the TableView to update when the CheckBox values changes.
todoItem -> new Observable[] { todoItem.completeProperty()}
Key Code 2
This code allows the FilteredList to set the TableView's data based on the Button that is pressed.
flToDoItems.setPredicate((todoItem) -> {
return todoItem.getComplete();
});
Main
import java.time.LocalDate;
import java.util.Arrays;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application
{
final TableView<ToDoItem> selectionTableView = new TableView<>();
private final ObservableList<ToDoItem> toDoItems = FXCollections.observableList(
Arrays.asList(
new ToDoItem("Task 1", "This is Task 1", LocalDate.now().plusMonths(1)),
new ToDoItem("Task 2", "This is Task 2", LocalDate.now().plusMonths(2)),
new ToDoItem("Task 3", "This is Task 3", LocalDate.now().plusMonths(3)),
new ToDoItem("Task 4", "This is Task 4", LocalDate.now().plusMonths(4)),
new ToDoItem("Task 5", "This is Task 5", LocalDate.now().plusMonths(5))
),
todoItem -> new Observable[] { todoItem.completeProperty()}//This is needed for automatic updates in the TableView!!!!
);
final FilteredList<ToDoItem> flToDoItems = new FilteredList(toDoItems, p -> true);
public static void main(String[] args)
{
launch();
}
#Override
public void start(Stage stage)
{
final TableView<ToDoItem> todoListSelectionTableView = createTodoListSelectionTableView();
todoListSelectionTableView.setItems(flToDoItems);
Button btnAllTasks = new Button("All");
btnAllTasks.setOnAction((t) -> {
flToDoItems.setPredicate((todoItem) -> {
return true;
});
});
Button btnCompleteTasks = new Button("Complete");
btnCompleteTasks.setOnAction((t) -> {
flToDoItems.setPredicate((todoItem) -> {
return todoItem.getComplete();
});
});
Button btnIncompleteTask = new Button("Incomplete");
btnIncompleteTask.setOnAction((t) -> {
flToDoItems.setPredicate((todoItem) -> {
return !todoItem.getComplete();
});
});
VBox root = new VBox(new HBox(btnAllTasks, btnIncompleteTask, btnCompleteTasks), todoListSelectionTableView);
Scene scene = new Scene(root, 500, 500);
stage.setScene(scene);
stage.show();
}
private TableView<ToDoItem> createTodoListSelectionTableView() {
selectionTableView.setPrefSize(440, 180);
TableColumn<ToDoItem, String> shortDescriptionColumn = new TableColumn<>("Short Description");
shortDescriptionColumn.setCellValueFactory(cd -> cd.getValue().shortDescriptionProperty());
selectionTableView.getColumns().add(shortDescriptionColumn);
TableColumn<ToDoItem, String> detailsColumn = new TableColumn<>("Details");
detailsColumn.setCellValueFactory(cd -> cd.getValue().detailsProperty());
selectionTableView.getColumns().add(detailsColumn);
TableColumn<ToDoItem, LocalDate> deadlineColumn = new TableColumn<>("Deadline");
deadlineColumn.setCellValueFactory(cd -> cd.getValue().deadlineProperty());
selectionTableView.getColumns().add(deadlineColumn);
TableColumn<ToDoItem, Boolean> completeColumn = new TableColumn<>("Complete");
completeColumn.setCellValueFactory(cd -> cd.getValue().completeProperty());
completeColumn.setCellFactory(CheckBoxTableCell.forTableColumn(completeColumn));
selectionTableView.getColumns().add(completeColumn);
selectionTableView.setEditable(true);
return selectionTableView;
}
}
ToDoItem
import java.time.LocalDate;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class ToDoItem {
private final StringProperty shortDescription;
private final StringProperty details;
private final ObjectProperty<LocalDate> deadline;
private final BooleanProperty complete;
public ToDoItem(String shortDescription, String details, LocalDate deadline) {
this.shortDescription = new SimpleStringProperty(shortDescription);
this.details = new SimpleStringProperty(details);
this.deadline = new SimpleObjectProperty(deadline);
this.complete = new SimpleBooleanProperty(false);
}
public String getShortDescription() {
return shortDescription.get();
}
public void setShortDescription(String shortDescription) {
this.shortDescription.set(shortDescription);
}
public StringProperty shortDescriptionProperty()
{
return this.shortDescription;
}
public String getDetails() {
return details.get();
}
public void setDetails(String details) {
this.details.set(details);
}
public StringProperty detailsProperty()
{
return this.details;
}
public LocalDate getDeadline() {
return deadline.get();
}
public void setDeadline(LocalDate deadline) {
this.deadline.set(deadline);
}
public ObjectProperty<LocalDate> deadlineProperty()
{
return this.deadline;
}
public void setComplete(boolean complete)
{
this.complete.set(complete);
}
public boolean getComplete()
{
return this.complete.get();
}
public BooleanProperty completeProperty()
{
return this.complete;
}
}
Output
I have a spell checker demo here, visually it is exactly what I want (red underline for words that are not correct), but I'm having trouble creating a right-click context menu to apply suggestions.
I was able to get a context menu on the Text object, but I was not able to find the position of the text in the box to replace using the prediction.
Here is the code:
pom.xml
<dependency>
<groupId>org.fxmisc.richtext</groupId>
<artifactId>richtextfx</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
<type>jar</type>
</dependency>
SpellCheckDemo.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.BreakIterator;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import org.apache.commons.text.similarity.JaroWinklerDistance;
import org.reactfx.Subscription;
public class SpellCheckingDemo extends Application
{
private static final Set<String> dictionary = new HashSet<String>();
private final static double JAROWINKLERDISTANCE_THRESHOLD = .80;
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage primaryStage)
{
StyleClassedTextArea textArea = new StyleClassedTextArea();
textArea.setWrapText(true);
Subscription cleanupWhenFinished = textArea.multiPlainChanges()
.successionEnds(Duration.ofMillis(500))
.subscribe(change ->
{
textArea.setStyleSpans(0, computeHighlighting(textArea.getText()));
});
// call when no longer need it: `cleanupWhenFinished.unsubscribe();`
textArea.setOnContextMenuRequested((ContextMenuEvent event) ->
{
if (event.getTarget() instanceof Text)
{
Text text = (Text) event.getTarget();
ContextMenu context = new ContextMenu();
JaroWinklerDistance distance = new JaroWinklerDistance();
for (String word : dictionary)
{
if (distance.apply(text.getText(), word) >= JAROWINKLERDISTANCE_THRESHOLD)
{
MenuItem item = new MenuItem(word);
item.setOnAction(a ->
{
// how do I find the position of the Text object ?
textArea.replaceText(25, 25 + text.getText().length(), word);
});
context.getItems().add(item);
}
}
context.show(primaryStage, event.getScreenX(), event.getScreenY());
}
});
// load the dictionary
try (InputStream input = SpellCheckingDemo.class.getResourceAsStream("/spellchecking.dict");
BufferedReader br = new BufferedReader(new InputStreamReader(input)))
{
String line;
while ((line = br.readLine()) != null)
{
dictionary.add(line);
}
} catch (IOException e)
{
e.printStackTrace();
}
// load the sample document
InputStream input2 = SpellCheckingDemo.class.getResourceAsStream("/spellchecking.txt");
try (java.util.Scanner s = new java.util.Scanner(input2))
{
String document = s.useDelimiter("\\A").hasNext() ? s.next() : "";
textArea.replaceText(0, 0, document);
}
Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(textArea)), 600, 400);
scene.getStylesheets().add(SpellCheckingDemo.class.getResource("/spellchecking.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setTitle("Spell Checking Demo");
primaryStage.show();
}
private static StyleSpans<Collection<String>> computeHighlighting(String text)
{
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
BreakIterator wb = BreakIterator.getWordInstance();
wb.setText(text);
int lastIndex = wb.first();
int lastKwEnd = 0;
while (lastIndex != BreakIterator.DONE)
{
int firstIndex = lastIndex;
lastIndex = wb.next();
if (lastIndex != BreakIterator.DONE
&& Character.isLetterOrDigit(text.charAt(firstIndex)))
{
String word = text.substring(firstIndex, lastIndex).toLowerCase();
if (!dictionary.contains(word))
{
spansBuilder.add(Collections.emptyList(), firstIndex - lastKwEnd);
spansBuilder.add(Collections.singleton("underlined"), lastIndex - firstIndex);
lastKwEnd = lastIndex;
}
System.err.println();
}
}
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
return spansBuilder.create();
}
}
The following files go into the resource folder:
spellchecking.css
.underlined {
-rtfx-background-color: #f0f0f0;
-rtfx-underline-color: red;
-rtfx-underline-dash-array: 2 2;
-rtfx-underline-width: 1;
-rtfx-underline-cap: butt;
}
spellchecking.dict
a
applied
basic
brown
but
could
document
dog
fox
here
if
is
its
jumps
lazy
no
over
quick
rendering
sample
see
styling
the
there
this
were
you
spellchecking.txt
The quik brown fox jumps over the lazy dog.
Ths is a sample dokument.
There is no styling aplied, but if there were, you could see its basic rndering here.
I found out how. By using the caret position, I can select a word and replace it. The problem is, right clicking didn't move the caret. So in order to move the caret, you add a listener.
textArea.setOnMouseClicked((MouseEvent mouseEvent) ->
{
if (mouseEvent.getButton().equals(MouseButton.SECONDARY))
{
if (mouseEvent.getClickCount() == 1)
{
CharacterHit hit = textArea.hit(mouseEvent.getX(), mouseEvent.getY());
int characterPosition = hit.getInsertionIndex();
// move the caret to that character's position
textArea.moveTo(characterPosition, SelectionPolicy.CLEAR);
}
}
});
Edit 1:
Added indexing and concurrency for performance purposes. Context menu is now instantaneous.
Edit 2:
Fixed macOS issue with context menu
Full code:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.PauseTransition;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.text.WordUtils;
import org.apache.commons.text.similarity.JaroWinklerSimilarity;
import org.fxmisc.richtext.CharacterHit;
import org.fxmisc.richtext.NavigationActions.SelectionPolicy;
public class SpellCheckingDemo extends Application
{
private static final int NUMBER_OF_SUGGESTIONS = 5;
private static final Set<String> DICTIONARY = ConcurrentHashMap.newKeySet();
private static final Map<String, List<String>> SUGGESTIONS = new ConcurrentHashMap<>();
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage primaryStage)
{
StyleClassedTextArea textArea = new StyleClassedTextArea();
textArea.setWrapText(true);
textArea.requestFollowCaret();
//wait a bit before typing has stopped to compute the highlighting
PauseTransition textAreaDelay = new PauseTransition(Duration.millis(250));
textArea.textProperty().addListener((observable, oldValue, newValue) ->
{
textAreaDelay.setOnFinished(event ->
{
textArea.setStyleSpans(0, computeHighlighting(textArea.getText()));
//have a new thread index all incorrect words, and pre-populate suggestions
Task task = new Task<Void>()
{
#Override
public Void call()
{
//iterating over entire list is ok because after the first time, it will hit the index anyway
for (String word : SpellCheckingDemo.SUGGESTIONS.keySet())
{
SpellCheckingDemo.getClosestWords(word);
SpellCheckingDemo.getClosestWords(StringUtils.trim(word));
}
return null;
}
};
new Thread(task).start();
});
textAreaDelay.playFromStart();
});
textArea.setOnMouseClicked((MouseEvent mouseEvent) ->
{
if (mouseEvent.getButton().equals(MouseButton.SECONDARY))
{
if (mouseEvent.getClickCount() == 1)
{
CharacterHit hit = textArea.hit(mouseEvent.getX(), mouseEvent.getY());
int characterPosition = hit.getInsertionIndex();
// move the caret to that character's position
if (StringUtils.isEmpty(textArea.getSelectedText()))
{
textArea.moveTo(characterPosition, SelectionPolicy.CLEAR);
}
if (mouseEvent.getTarget() instanceof Text && StringUtils.isEmpty(textArea.getSelectedText()))
{
textArea.selectWord();
//When selecting right next to puncuation and spaces, the replacements elimantes these values. This avoids the issue by moving the caret towards the middle
if (!StringUtils.isEmpty(textArea.getSelectedText()) && !CharUtils.isAsciiAlphanumeric(textArea.getSelectedText().charAt(textArea.getSelectedText().length() - 1)))
{
textArea.moveTo(textArea.getCaretPosition() - 2);
textArea.selectWord();
}
String referenceWord = textArea.getSelectedText();
textArea.deselect();
if (!NumberUtils.isParsable(referenceWord) && !DICTIONARY.contains(StringUtils.trim(StringUtils.lowerCase(referenceWord))))
{
ContextMenu context = new ContextMenu();
for (String word : SpellCheckingDemo.getClosestWords(referenceWord))
{
MenuItem item = new MenuItem(word);
item.setOnAction((ActionEvent a) ->
{
textArea.selectWord();
textArea.replaceSelection(word);
textArea.deselect();
});
context.getItems().add(item);
}
if (!context.getItems().isEmpty())
{
textArea.moveTo(textArea.getCaretPosition() - 1);
context.show((Node) mouseEvent.getTarget(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
((Node) mouseEvent.getTarget()).addEventHandler(MouseEvent.MOUSE_PRESSED, event -> context.hide());
} else
{
ContextMenu copyPasteMenu = getCopyPasteMenu(textArea);
copyPasteMenu.show((Node) mouseEvent.getTarget(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
((Node) mouseEvent.getTarget()).addEventHandler(MouseEvent.MOUSE_PRESSED, event -> copyPasteMenu.hide());
}
} else
{
ContextMenu copyPasteMenu = getCopyPasteMenu(textArea);
copyPasteMenu.show((Node) mouseEvent.getTarget(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
((Node) mouseEvent.getTarget()).addEventHandler(MouseEvent.MOUSE_PRESSED, event -> copyPasteMenu.hide());
}
} else
{
ContextMenu copyPasteMenu = getCopyPasteMenu(textArea);
copyPasteMenu.show((Node) mouseEvent.getTarget(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
((Node) mouseEvent.getTarget()).addEventHandler(MouseEvent.MOUSE_PRESSED, event -> copyPasteMenu.hide());
}
}
}
});
// load the dictionary
try (InputStream input = SpellCheckingDemo.class.getResourceAsStream("/spellchecking.dict");
BufferedReader br = new BufferedReader(new InputStreamReader(input)))
{
String line;
while ((line = br.readLine()) != null)
{
DICTIONARY.add(line);
}
} catch (IOException ex)
{
Logger.getLogger(SpellCheckingDemo.class.getName()).log(Level.SEVERE, null, ex);
}
// load the sample document
InputStream input2 = SpellCheckingDemo.class.getResourceAsStream("/spellchecking.txt");
try (java.util.Scanner s = new java.util.Scanner(input2))
{
String document = s.useDelimiter("\\A").hasNext() ? s.next() : "";
textArea.replaceText(0, 0, document);
}
Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(textArea)), 600, 400);
scene.getStylesheets().add(SpellCheckingDemo.class.getResource("/spellchecking.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setTitle("Spell Checking Demo");
primaryStage.show();
}
private static StyleSpans<Collection<String>> computeHighlighting(String text)
{
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
BreakIterator wb = BreakIterator.getWordInstance();
wb.setText(text);
int lastIndex = wb.first();
int lastKwEnd = 0;
while (lastIndex != BreakIterator.DONE)
{
int firstIndex = lastIndex;
lastIndex = wb.next();
if (lastIndex != BreakIterator.DONE && Character.isLetterOrDigit(text.charAt(firstIndex)))
{
String word = text.substring(firstIndex, lastIndex);
if (!NumberUtils.isParsable(word) && !DICTIONARY.contains(StringUtils.lowerCase(word)))
{
spansBuilder.add(Collections.emptyList(), firstIndex - lastKwEnd);
spansBuilder.add(Collections.singleton("underlined"), lastIndex - firstIndex);
lastKwEnd = lastIndex;
SpellCheckingDemo.SUGGESTIONS.putIfAbsent(word, Collections.emptyList());
}
//System.err.println();
}
}
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
return spansBuilder.create();
}
public static List<String> getClosestWords(String word)
{
//check to see if an suggestions for this word have already been indexed
if (SpellCheckingDemo.SUGGESTIONS.containsKey(word) && !SpellCheckingDemo.SUGGESTIONS.get(word).isEmpty())
{
return SpellCheckingDemo.SUGGESTIONS.get(word);
}
List<StringDistancePair> allWordDistances = new ArrayList<>(DICTIONARY.size());
String lowerCaseWord = StringUtils.lowerCase(word);
JaroWinklerSimilarity jaroWinklerAlgorithm = new JaroWinklerSimilarity();
for (String checkWord : DICTIONARY)
{
allWordDistances.add(new StringDistancePair(jaroWinklerAlgorithm.apply(lowerCaseWord, checkWord), checkWord));
}
allWordDistances.sort(Comparator.comparingDouble(StringDistancePair::getDistance));
List<String> closestWords = new ArrayList<>(NUMBER_OF_SUGGESTIONS);
System.out.println(word);
for (StringDistancePair pair : allWordDistances.subList(allWordDistances.size() - NUMBER_OF_SUGGESTIONS, allWordDistances.size()))
{
// 0 is not a match at all, so no point adding to list
if (pair.getDistance() == 0.0)
{
continue;
}
String addWord;
if (StringUtils.isAllUpperCase(word))
{
addWord = StringUtils.upperCase(pair.getWord());
} else if (CharUtils.isAsciiAlphaUpper(word.charAt(0)))
{
addWord = WordUtils.capitalize(pair.getWord());
} else
{
addWord = StringUtils.lowerCase(pair.getWord());
}
System.out.println(pair);
closestWords.add(addWord);
}
System.out.println();
Collections.reverse(closestWords);
//add the suggestion list to index to allow future pulls
SpellCheckingDemo.SUGGESTIONS.put(word, closestWords);
return closestWords;
}
public static ContextMenu getCopyPasteMenu(StyleClassedTextArea textArea)
{
ContextMenu context = new ContextMenu();
MenuItem cutItem = new MenuItem("Cut");
cutItem.setOnAction((ActionEvent a) ->
{
Clipboard clipboard = Clipboard.getSystemClipboard();
ClipboardContent content = new ClipboardContent();
content.putString(textArea.getSelectedText());
clipboard.setContent(content);
textArea.replaceSelection("");
});
context.getItems().add(cutItem);
MenuItem copyItem = new MenuItem("Copy");
copyItem.setOnAction((ActionEvent a) ->
{
Clipboard clipboard = Clipboard.getSystemClipboard();
ClipboardContent content = new ClipboardContent();
content.putString(textArea.getSelectedText());
clipboard.setContent(content);
});
context.getItems().add(copyItem);
MenuItem pasteItem = new MenuItem("Paste");
pasteItem.setOnAction((ActionEvent a) ->
{
Clipboard clipboard = Clipboard.getSystemClipboard();
if (!StringUtils.isEmpty(textArea.getSelectedText()))
{
textArea.replaceSelection(clipboard.getString());
} else
{
textArea.insertText(textArea.getCaretPosition(), clipboard.getString());
}
});
context.getItems().add(pasteItem);
context.getItems().add(new SeparatorMenuItem());
MenuItem selectAllItem = new MenuItem("Select All");
selectAllItem.setOnAction((ActionEvent a) ->
{
textArea.selectAll();
});
context.getItems().add(selectAllItem);
if (StringUtils.isEmpty(textArea.getSelectedText()))
{
cutItem.setDisable(true);
copyItem.setDisable(true);
}
return context;
}
private static class StringDistancePair
{
private final double x;
private final String y;
public StringDistancePair(double x, String y)
{
this.x = x;
this.y = y;
}
public String getWord()
{
return y;
}
public double getDistance()
{
return x;
}
#Override
public String toString()
{
return StringUtils.join(String.valueOf(getDistance()), " : ", String.valueOf(getWord()));
}
}
}
Download the full English dictionary here:
https://github.com/dwyl/english-words/blob/master/words_alpha.txt
I have a memory leak in my application. After profiling with VisualVM a found out that a SortedList in a TableView retains instances of removed objects at the end of its internal sorted array.
How should i try resolving this?
Using jdk1.8.0_141
Code to demonstrate the problem below (click the button below the table and you'll get an OutOfMemoryError eventually) :
import java.util.*;
import java.util.stream.*;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.*;
import javafx.collections.transformation.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class SortedListTest extends Application {
public static void main(String[] args) {
launch(args);
}
int universeSize = 500;
Universe universe = createUniverse();
ObservableList<Entity> currentEntities = FXCollections.observableArrayList();
#Override
public void start(Stage primaryStage) throws Exception {
currentEntities.addAll(universe.entities);
MyTable table = new MyTable(currentEntities);
TableColumn<Entity, UUID> c1 = new TableColumn<>("C1");
c1.setCellValueFactory(c -> c.getValue().prop1);
table.getColumns().addAll(c1);
Button btn = new Button();
btn.setText(Integer.toString(universeSize));
btn.setOnAction(e -> {
universe = createUniverse();
currentEntities.setAll(universe.entities);
btn.setText(Integer.toString(universeSize));
});
primaryStage.setScene(new Scene(new VBox(table, btn)));
primaryStage.show();
}
Universe createUniverse() {
return new Universe(universeSize--);
}
static class Universe {
final ObservableList<Entity> entities;
final int size;
Universe(int size) {
this.size = size;
this.entities = FXCollections.observableArrayList(
IntStream.range(0, size)
.mapToObj(i -> new Entity(this))
.collect(Collectors.toList())
);
}
}
static class Entity implements Comparable<Entity> {
final byte[] blob = new byte[1024 * 1024];
final SimpleObjectProperty<UUID> prop1 = new SimpleObjectProperty<>(UUID.randomUUID());
final Universe universe;
final double filter = Math.random();
public Entity(Universe universe) {
this.universe = universe;
}
#Override
public int compareTo(Entity o) {
return prop1.get().compareTo(o.prop1.get());
}
}
static class MyTable extends TableView<Entity> {
private ObservableList<Entity> backingList;
private SortedList<Entity> sortedList;
public MyTable(ObservableList<Entity> items) {
super(items);
itemsProperty().addListener(o -> {
ObservableList<Entity> list = getItems();
if (getItems() != sortedList) {
setFilteredList(list);
}
});
setFilteredList(getItems());
}
private void setFilteredList(ObservableList<Entity> backingList) {
this.backingList = backingList;
Comparator<Entity> comparator = sortedList != null ? (Comparator<Entity>) sortedList.getComparator() : Comparator.naturalOrder();
sortedList = backingList.sorted(comparator);
sortedList.comparatorProperty().bind(comparatorProperty());
setItems(sortedList);
}
}
}
In real code the separate lists in the table are used for some additional functionality.
I have written this little example application:
package application;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
public class Person {
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
public Person(String firstName, String lastName) {
this.firstName.set(firstName);
this.lastName.set(lastName);
}
public String getFirstName() {
return firstName.get();
}
public String getLastName() {
return lastName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public StringProperty lastNameProperty() {
return lastName;
}
}
#Override
public void start(Stage primaryStage) {
try {
StackPane root = new StackPane();
TableView<Person> tv = new TableView<>();
TableColumn<Person, String> col = new TableColumn<Person, String>("FirstName");
col.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
tv.getColumns().add(col);
tv.setEditable(true);
col = new TableColumn<Person, String>("LastName");
col.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
col.setCellFactory(TextFieldTableCell.forTableColumn());
col.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Person, String>>() {
#Override
public void handle(CellEditEvent<Person, String> event) {
System.out.println(tv.getItems().get(1).getLastName());
}
});
tv.getColumns().add(col);
for (int i = 0; i < 30; i++) {
tv.getItems().add(new Person("Test" + i, "Test" + i));
}
root.getChildren().add(tv);
Scene scene = new Scene(root, 400, 200);
primaryStage.setScene(scene);
primaryStage.show();
tv.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> {
// ...
});
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
I want to perform action when the ScrollBar has reached the bottom. Then I want to reload more datas from the database. But only then, when the user has seen all the already loaded datas (= scrollbar on the bottom). Do you have nice suggestions to solve this issue?
My first idea was to catch the MOUSE_RELEASED event (when the users drags the bar) of the TableView and then to check the position of the ScrollBar:
- getValue() gets the position of the bar
- getMax() the maximum value (=bottom).
But I can't find a way (without using the css-selector via this method) to get the ScrollBar from a given TableView. So I can't check the position of it in a certain TableView.
Do you have any ideas??
I am excited. Thanks for your help.
The only way to get the scroll bar is via a lookup, which is a bit of a hack, but it will work as long as you do it after the table has been rendered on the scene. You need
ScrollBar verticalBar = (ScrollBar) table.lookup(".scroll-bar:vertical");
Note that there's no need to mess with user events: you can just observe the scroll bar's value property directly:
verticalBar.valueProperty().addListener((obs, oldValue, newValue) -> {
if (newValue.doubleValue() >= verticalBar.getMax()) {
// add more data...
}
});
SSCCE:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AddMoreTableDataOnScrollToBottom extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
addMoreData(table, 20);
Scene scene = new Scene(new BorderPane(table), 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
ScrollBar verticalBar = (ScrollBar) table.lookup(".scroll-bar:vertical");
verticalBar.valueProperty().addListener((obs, oldValue, newValue) -> {
if (newValue.doubleValue() >= verticalBar.getMax()) {
addMoreData(table, 20);
}
});
}
private void addMoreData(TableView<Item> table, int numItems) {
Task<List<Item>> dataRetrieveTask = new Task<List<Item>>() {
#Override
public List<Item> call() throws Exception {
// mimic connect to db:
Thread.sleep(500);
List<Item> items = new ArrayList<>();
int nextItem = table.getItems().size() + 1 ;
for (int i = nextItem; i < nextItem + numItems; i++ ){
items.add(new Item("Item "+i, i));
}
return items ;
}
};
dataRetrieveTask.setOnSucceeded(e -> table.getItems().addAll(dataRetrieveTask.getValue()));
new Thread(dataRetrieveTask).start();
}
private <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> prop) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> prop.apply(cellData.getValue()));
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
I wrote a thread that checks constantly if the mouse is over a ListView, because I want to show a Popup containing info about the cell I point with the mouse.
So no problem to check if the mouse is over the ListView.
But how do I check if the mouse is over a certain cell since I cannot use ListCell.localToScreen(ListCell.getBoundsInLocal()); to get the cell coordinates on screen?
I prefer not to use ListCell event such as onMouseEntered.
Either register handlers for mouseEntered and mouseExited events on each ListCell, or observe the ListCell's hoverProperty. Here's an example using the second method:
import java.util.stream.IntStream;
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Popup;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PopupOnListCellHover extends Application {
private Popup popup ;
private Node popupContent ;
private Label titleLabel ;
private Label detailsLabel ;
private FadeTransition fadeOut ;
#Override
public void start(Stage primaryStage) {
ListView<Item> listView = new ListView<>();
popup = new Popup();
titleLabel = new Label();
titleLabel.setStyle("-fx-font-size: 1.5em ; -fx-font-weight: bold;");
detailsLabel = new Label();
popupContent = new VBox(10, titleLabel, detailsLabel);
popupContent.setStyle("-fx-background-color: -fx-background; "+
"-fx-background: lightskyblue; -fx-padding:12px;");
popup.getContent().add(popupContent);
fadeOut = new FadeTransition(Duration.millis(500), popupContent);
fadeOut.setFromValue(1.0);
fadeOut.setToValue(0.0);
fadeOut.setOnFinished(e -> popup.hide());
listView.setCellFactory(lv -> {
ListCell<Item> cell = new ListCell<Item>() {
#Override
public void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(item.getName());
}
}
};
cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered && ! cell.isEmpty()) {
showPopup(cell);
} else {
hidePopup();
}
});
return cell ;
});
IntStream.rangeClosed(1, 100).mapToObj(i -> new Item("Item "+i, i))
.forEach(listView.getItems()::add);
BorderPane root = new BorderPane(listView);
Scene scene = new Scene(root, 250, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private void showPopup(ListCell<Item> cell) {
fadeOut.stop();
popupContent.setOpacity(1.0);
Bounds bounds = cell.localToScreen(cell.getBoundsInLocal());
popup.show(cell, bounds.getMaxX(), bounds.getMinY());
Item item = cell.getItem() ;
titleLabel.setText(item.getName());
detailsLabel.setText(String.format("This is %s.%nIt has value %d.",
item.getName(), item.getValue()));
}
private void hidePopup() {
fadeOut.playFromStart();
}
public static class Item {
private final int value ;
private final String name ;
public Item(String name, int value) {
this.name = name ;
this.value = value ;
}
public int getValue() {
return value ;
}
public String getName() {
return name ;
}
}
public static void main(String[] args) {
launch(args);
}
}
To use handlers for mouseEntered and mouseExited, replace
cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered && ! cell.isEmpty()) {
showPopup(cell);
} else {
hidePopup();
}
});
with
cell.setOnMouseEntered(e -> showPopup(cell));
cell.setOnMouseExited(e -> hidePopup());