Last active
February 21, 2022 13:11
-
-
Save kleopatra/447344183e017537c21f7905a062396d to your computer and use it in GitHub Desktop.
Sneak preview: api changes to support commitOnFocusLost
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| * 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