Skip to content

Instantly share code, notes, and snippets.

@kleopatra
Last active February 21, 2022 13:11
Show Gist options
  • Select an option

  • Save kleopatra/447344183e017537c21f7905a062396d to your computer and use it in GitHub Desktop.

Select an option

Save kleopatra/447344183e017537c21f7905a062396d to your computer and use it in GitHub Desktop.
Sneak preview: api changes to support commitOnFocusLost
* Created 02.05.2021
*/
package control.cell.edit;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.EditState.EditAction;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
/**
* Visual test for testing commitOnFocusLost (ListView only) against draft
* with EditState.
*
*
* It has
*
* - enriched edit location property: EditState has both the location and the
* type of the state change
* - ListView: having the EditState property (and keeping it in sync with old editingIndex)
* - ListView: complete edit state change api (start, commit, cancel, stop)
* - Cell: extracted handler on focusLost for override in subs, api to access the editedValue
* - ListCell: using new ListView api - listening to editState, use edit api, overridden focusedLost
* to delegate to ListView.stopEdit
* - ListCellBehavior: use start/stopEdit of listView
* - TextFieldTableCell: implement editedValue
*
*
* branch: https://github.com/kleopatra/jfx/tree/dokeep-edit-api-editstate
*
* -------------
*
*
* scenarios (default): note that the fix requires the default action in ListView
* to be set to commit (otherwise it behaves the same as previously)
*
* Note: "with fix" currently means all of the above (ListView only)
*
* A: edit selected item -> start edit next/prev with external key (F1)
* expected: edit committed, next/prev editing
* with fix: edit committed
* without fix: edit cancelled
*
* A1: not relevant ? same as A, but now code moving selection before editing
* with fix: edit cancelled (due to cancel from focus listener in cell?)
* without: edit cancelled
*
* A2: not relevant ? same as A, but now code moving selection after editing
* with fix: working as expected
* without: edit cancelled
*
* B: edit selected item, click into other not-empty cell
* expected: edit committed, (cell selected/focused?)
* with fix: edit committed (selection on clicked/focus unchanged)
* without: edit cancelled (selection/focus on that cell?)
*
* B1: edit selected item, click into other empty cell
* expected: edit committed
* with fix: text field looses focus with content unchanged, cell appears "selected" (blue background)
* without: same as with
*
* C: edit focused cell (no selection), move focus
* expected: edit committed
* with fix: edit committed, old index selected/list (or tab) focused?
* without: edit cancelled, old index selected
*
* C1: edit focused cell (no selection), move selection
* expected: edit committed
* with fix: edit committed, new cell focused and selected
* without: edit cancelled, new index selected
*
* D: edit selected cell, move focus
* expected: edit committed
* with fix: edit committed, old index selected/list (or tab) focused?
* without: edit cancelled, old index selected
*
* D1: edit selected cell, move selection
* expected: edit committed
* with fix: edit committed, new cell selected /list (or tab) focused?
* without: edit cancelled
*
* D2 Note: - spurious problem with selectNext? focus not moved as well?
* if so:
* with fix: ??
* without: cell still editing
*
* ---- ListView
*
* scenario: move focus to component outside of control
* - expected: edit stopped and committed
* - actual: control still editing
* might be incorrect expectation? yes, probably - but can be implemented by a listener
* to scene focusOwner and manually stop if new owner is not child of the control
*
* To reproduce
* - edit cell, type something
* - focus one of the dummies below
* - expected: editing stopped (at least) and new value committed (bug)
* - actual: cell still editing
*
*/
public class ListEditStateCommitOnFocusLost extends Application {
private TabPane tabPane;
private Scene scene;
// special casing listView
ListView<String> listView;
// commit from listener to focusOwner
private BooleanProperty commitOnLostOutside = new SimpleBooleanProperty(false);
// unused?
// move selection in editNext/previous (before starting edit)
private boolean moveSelectionBefore = false;
private boolean moveSelectionAfter = false;
@Override
public void start(Stage primaryStage) {
// tabPane for all
tabPane = new TabPane(
new Tab("List", createListView()),
new Tab("Table", createTableView()),
new Tab("Dummy")
);
BorderPane root = new BorderPane(tabPane);
root.setBottom(new HBox(10, new Button("dummy for focus")));
scene = new Scene(root, 300, 400);
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.F1), this::editNext);
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.F1, KeyCombination.CONTROL_DOWN), this::editPrevious);
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.F3), this::focusNextCell);
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.F4), this::selectNextCell);
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.F6), this::commit);
// quick check: implement stopEdit on focus lost to external control
scene.focusOwnerProperty().addListener((src, ov, nv) -> {
if (commitOnLostOutside.get() && childOf(ov, listView) && !(childOf(nv, listView))) {
listView.stopEdit();
}
});
root.setTop(new FlowPane(10, 10,
new Label("F1 - editNext"),
new Label("ctrl-F1 - editPrevious"),
new Label("F3 - focusNextCell"),
new Label("F4 - selectNextCell"),
new Label("F6 - commit"),
new Label()
));
CheckBox commitOutside = new CheckBox("commitOnOutside");
commitOutside.selectedProperty().bindBidirectional(commitOnLostOutside);
root.setBottom(new FlowPane(10, 10, commitOutside));
primaryStage.setScene(scene);
primaryStage.setX(10);
primaryStage.show();
}
private boolean childOf(Node child, Parent parent) {
if (child == null || child == parent) return true;
Parent current = child.getParent();
while (current != null) {
if (current == parent) return true;
current = current.getParent();
}
return false;
}
private void selectNextCell() {
Node content = getSelectedTabContent();
if (content instanceof ListView) {
ListView<?> listView = (ListView<?>) content;
listView.getSelectionModel().selectNext();
}
}
private void focusNextCell() {
Node content = getSelectedTabContent();
if (content instanceof ListView) {
ListView<?> listView = (ListView<?>) content;
listView.getFocusModel().focusNext();
}
}
private void editNext() {
Node content = getSelectedTabContent();
if (content instanceof ListView) {
ListView<?> listView = (ListView) content;
int editingIndex = listView.getEditingIndex();
if (moveSelectionBefore) listView.getSelectionModel().selectNext();
listView.edit(Math.min(editingIndex +1, listView.getItems().size() -1));
if (moveSelectionAfter) listView.getSelectionModel().selectNext();
}
}
private void editPrevious() {
Node content = getSelectedTabContent();
if (content instanceof ListView) {
ListView<?> listView = (ListView) content;
int editingIndex = listView.getEditingIndex();
if (moveSelectionBefore) listView.getSelectionModel().selectPrevious();
listView.edit(Math.max(0, editingIndex - 1));
if (moveSelectionAfter) listView.getSelectionModel().selectPrevious();
}
}
private void commit() {
Node content = getSelectedTabContent();
if (content instanceof ListView) {
ListView<?> listView = (ListView) content;
listView.commitEdit();
}
}
// access virtualized control
private Node getSelectedTabContent() {
if (tabPane == null) {
Node root = scene.getRoot();
if (root instanceof BorderPane) {
return ((BorderPane) root).getCenter();
}
return null;
}
Tab selected = tabPane.getSelectionModel().getSelectedItem();
Node content = selected.getContent();
if (content instanceof BorderPane) {
content = ((BorderPane) content).getCenter();
}
return content;
}
private BorderPane createListView() {
ListView<String> simpleList = new ListView<>(FXCollections
.observableArrayList("Item1", "Item2", "Item3", "Item4")) ;
simpleList.setCellFactory(TextFieldListCell.forListView());
simpleList.setEditable(true);
simpleList.setDefaultStopEditAction(EditAction.COMMIT);
/**
* Implement editNext in a listener.
* Problem: can't distinguish source of state change - we want to
* act only if the commit was triggered by a user gesture in the cell.
*/
simpleList.editStateProperty().addListener((src, ov, nv) -> {
// System.out.println("EditState property change: " + ov + " " + nv);
});
simpleList.addEventHandler(ListView.editCommitEvent(), t -> {
System.out.println(t.getEventType() + " on " + t.getIndex());
if (t.getEventType() == ListView.editCommitEvent()) {
}
if (t.getEventType() == ListView.editCancelEvent()) {
// new RuntimeException("who? ").printStackTrace();
}
});
simpleList.addEventHandler(ListView.editCancelEvent(), t -> {
System.out.println(t.getEventType() + " on " + t.getIndex());
});
listView = simpleList;
BorderPane tabContent = new BorderPane(simpleList);
tabContent.setBottom(new HBox(10, new TextField("focus target on Tab")));
return tabContent;
}
/**
* For tableView we need an extractor on the property to see the cancel.
*/
private TableView<MenuItem> createTableView() {
ObservableList<MenuItem> itemsWithExtractor = FXCollections.observableArrayList(
p -> new Observable[] {p.textProperty()});
itemsWithExtractor.addAll(new MenuItem("first"), new MenuItem("second"), new MenuItem("third"));
TableView<MenuItem> table = new TableView<>(itemsWithExtractor);
table.setEditable(true);
TableColumn<MenuItem, String> name = new TableColumn<>("Last Name");
name.setCellFactory(TextFieldTableCell.forTableColumn());
name.setCellValueFactory(cc -> cc.getValue().textProperty());
TableColumn<MenuItem, String> style = new TableColumn<>("Last Name");
style.setCellFactory(TextFieldTableCell.forTableColumn());
style.setCellValueFactory(cc -> cc.getValue().styleProperty());
table.getColumns().addAll(name, style);
name.addEventHandler(TableColumn.editAnyEvent(), t ->
System.out.println(t.getEventType() + " on " + t.getTablePosition().getRow()));
return table;
}
public static void main(String[] args) {
launch(args);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment