JavaFX tableview auto scroll to selected item when pressing a button to selectNext() or selectPrevious() - javafx

I'm writing a JavaFX program with a TableView called 'table' and 2 buttons called 'previous' & 'next'.
Here is part of the code:
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
});
next.setOnAction(event -> {
table.getSelectionModel().selectNext();
});
However, if I keep pressing the buttons, the table will not scroll automatically to keep the selected item visible. So I modified the code like this :
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
table.scrollTo(table.getSelectionModel().getSelectedItem());
});
next.setOnAction(event -> {
table.getSelectionModel().selectNext();
table.scrollTo(table.getSelectionModel().getSelectedItem());
});
But it will always try to keep the selected item at the top of the visible region. If I keep pressing 'next'. The selected item will stay at the top instead of staying at the bottom.
I want to mimic the natural behavior of a tableview in the way that if I press up or down on the keyboard with something selected, the tableview will scroll automatically to keep the selected item visible.
How should I modify the code to make the auto scrolling more natural when I press the buttons?
Thanks

The problem is
missing fine-grained control of scrollTo target location on application level
the (somewhat unfortunate) implementation of virtualizedControl.scrollTo(index) which (ultimately) leads to calling flow.scrollToTop(index)
There's a long-standing RFE (reported 2014!) requesting better control from application code. Actually, VirtualFlow has public methods (scrollToTop, scrollTo, scrollPixels) providing such, only they are not passed on to the control layer (getVirtualFlow in VirtualContainerBase is final protected), so can't be overridden in a custom skin. Since fx12, we can hack a bit, and expose the onSelectXX of Tree/TableViewSkin and use those, either directly in application code (example below) or in a custom TableView.
Example code:
public class TableSelectNextKeepVisible extends Application {
/**
* Custom table skin to expose onSelectXX methods for application use.
*/
public static class MyTableSkin<T> extends TableViewSkin<T> {
public MyTableSkin(TableView<T> control) {
super(control);
}
/**
* Overridden to widen scope to public.
*/
#Override
public void onSelectBelowCell() {
super.onSelectBelowCell();
}
/**
* Overridden to widen scope to public.
*/
#Override
public void onSelectAboveCell() {
super.onSelectAboveCell();
}
}
private Parent createContent() {
TableView<Locale> table = new TableView<>(FXCollections.observableArrayList(Locale.getAvailableLocales())) {
#Override
protected Skin<?> createDefaultSkin() {
return new MyTableSkin<>(this);
}
};
TableColumn<Locale, String> country = new TableColumn<>("Column");
country.setCellValueFactory(new PropertyValueFactory<>("displayLanguage"));
table.getColumns().addAll(country);
Button next = new Button("next");
next.setOnAction(e -> {
table.getSelectionModel().selectNext();
// scrolls to top
// table.scrollTo(table.getSelectionModel().getSelectedIndex());
((MyTableSkin<?>) table.getSkin()).onSelectBelowCell();
});
Button previous = new Button("previous");
previous.setOnAction(e -> {
table.getSelectionModel().selectPrevious();
// scrolls to top
// table.scrollTo(table.getSelectionModel().getSelectedIndex());
((MyTableSkin<?>) table.getSkin()).onSelectAboveCell();
});
BorderPane content = new BorderPane(table);
content.setBottom(new HBox(10, next, previous));
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

try using getSelectedIndex as follows instead of using getSelectedItem
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
table.scrollTo(table.getSelectionModel().getSelectedIndex());
});

Platform.runLater( () -> TABLE_NAME.scrollTo(TABLE_INFORMATION_LIST.getList().size()-index) );
should work if you call it whenever you add information to the table.

Related

JavaFX focus on tab's content upon switch

I have a TabPane with a TextArea inside each of its Tabs.
What I want to achieve is when switching tabs, the textArea get focused.
I tried with a listener but it doesn't seem to work :
#FXML
public void initialize() {
for(Tab tab : tabPane.getTabs())
{
tab.setOnSelectionChanged(event->
{
if(tab.isSelected())
{
System.out.println(tab.getText());
TextArea ta = (TextArea)((AnchorPane)tab.getContent()).getChildren().get(0);
ta.requestFocus();
}
});
}
}
When I switch tabs, the output shows the active tab title but it stays focused, how can I focus on the TextArea after switching?
Thanks!
While it's not unusual that node.requestFocus() doesn't focus the node as expected (with the usual slightly smelly way around of wrapping it into Platform.runlater()) I'm interested why exactly it doesn't work in this context.
Turned out that one technical reason is that at the time of getting notified by any of the selection properties (selectedItem/-Index, isSelected) the node is not yet in a visible parent hierarchy - so it can't be a valid focus target. To see, add a println to the onSelected handler:
Node tabContent = tab.getContent();
if (tab.isSelected() && tab.getContent() != null && tab.getContent().getParent() != null ) {
System.out.println("onSelection " + tab.getText()
+ tabContent.getParent().isVisible());
}
That is due to skin's layout/management of tabs: the content of each is wrapped into a specialized StackPane (TabContentRegion), all these are stacked on top of each other with only the selected with its visibility property true.
So a first approximation for a solution is to register a listener to the visibility property of that container: when changed to true, its children should be eligable as focus targets. Which in fact they are .. just .. the TabPaneBehavior is interfering by forcing the focus onto the tabPane itself whenever selection is changed by user interaction (both by clicking the tab header and using ctrl-tab)
// unconditionally by mouse
new MouseMapping(MouseEvent.MOUSE_PRESSED, e -> getNode().requestFocus())
// method called by keyMappings that move the selection
private void moveSelection(int startIndex, int delta) {
final TabPane tabPane = getNode();
if (tabPane.getTabs().isEmpty()) return;
int tabIndex = findValidTab(startIndex, delta);
if (tabIndex > -1) {
final SelectionModel<Tab> selectionModel = tabPane.getSelectionModel();
selectionModel.select(tabIndex);
}
tabPane.requestFocus();
}
Next round: let the tabPane pass-on the focus whenever it gets focused during selection change. One sentence posing two stumble stones:
there is no public api to support transfer focus, it must be hacked around, f.i. by manually firing a TAB
during selection change needs state logic to decide its start and end
In all, looks like a task for a custom skin which is outlined (beware: not formally tested!) in the example below (it's for fx11, fx8 might be similar but requires to access internal classes because skins are not yet public)
public class TabPaneFocusOnSelectionSO extends Application {
/**
* Custom skin that tries to focus the first child of selected tab when
* selection changed.
*
*/
public static class MyTabPaneSkin extends TabPaneSkin {
private boolean selecting = true;
/**
* #param control
*/
public MyTabPaneSkin(TabPane control) {
super(control);
// TBD: dynamic update on changing tabs at runtime
addTabContentVisibilityListener(getChildren());
registerChangeListener(control.focusedProperty(), this::focusChanged);
registerChangeListener(control.getSelectionModel().selectedItemProperty(), e -> {
selecting = true;
});
}
/**
* Callback from listener to skinnable's focusedProperty.
*
* #param focusedProperty the property that's changed
*/
protected void focusChanged(ObservableValue focusedProperty) {
if (getSkinnable().isFocused() && selecting) {
transferFocus();
selecting = false;
}
}
/**
* Callback from listener to tab visibility.
*
* #param visibleProperty the property that's changed
*/
protected void tabVisibilityChanged(ObservableValue visibleProperty) {
BooleanProperty b = (BooleanProperty) visibleProperty;
if (b.get()) {
transferFocus();
}
}
/**
* No public api to transfer focus "away" from any node, hack by firing
* a TAB key on the TabPane.
*/
protected void transferFocus() {
final KeyEvent tabEvent = new KeyEvent(KeyEvent.KEY_PRESSED, "", "",
KeyCode.TAB, false, false, false, false);
Event.fireEvent(getSkinnable(), tabEvent);
}
/**
* Register the visibilityListener to each child in the given list that
* is a TabContentArea.
*
*/
protected void addTabContentVisibilityListener(List<? extends Node> children) {
children.forEach(node -> {
if (node.getStyleClass().contains("tab-content-area")) {
registerChangeListener(node.visibleProperty(), this::tabVisibilityChanged);
}
});
}
}
private TabPane tabPane;
private Parent createContent() {
tabPane = new TabPane() {
#Override
protected Skin<?> createDefaultSkin() {
return new MyTabPaneSkin(this);
}
};
for (int i = 0; i < 3; i++) {
VBox tabContent = new VBox();
tabContent.getChildren().addAll(new Button("dummy " +i), new TextField("just a field " + i));
Tab tab = new Tab("Tab " + i, tabContent);
tabPane.getTabs().add(tab);
}
tabPane.getTabs().add(new Tab("no content"));
tabPane.getTabs().add(new Tab("not focusable content", new Label("me!")));
BorderPane content = new BorderPane(tabPane);
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.setTitle(" TabPane with custom skin ");
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

How do I stop TextArea from listening to Shortcut KeyCombinations as KeyEvents?

Just as the title says, how do I stop shortcut keys (accelerators) being picked up as key events in TextArea? I have tried the method suggested here with different modifications: TextArea ignore KeyEvent in JavaFX with no luck.
If you want to stop specific accelerators from working when the TextArea has focus simply add an event filter for KEY_PRESSED events.
public class AcceleratorFilter implements EventHandler<KeyEvent> {
// blacklist of KeyCombinations
private final Set<KeyCombination> combinations;
public AcceleratorFilter(KeyCombination... combinations) {
this.combinations = Set.of(combinations);
}
#Override
public void handle(Event event) {
if (combinations.stream().anyMatch(combo -> combo.match(event)) {
event.consume();
}
}
}
TextArea area = new TextArea();
area.addEventFilter(KeyEvent.KEY_PRESSED, new AcceleratorFilter(
KeyCombination.valueOf("shortcut+o"),
KeyCombination.valueOf("shortcut+s") // etc...
));
If you want to indiscriminately block all accelerators registered with the Scene then you can query the Scenes accelerators and consume the KeyEvent if appropriate.
TextArea area = new TextArea();
area.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var scene = ((Node) event.getSource()).getScene();
// #getAccelerators() = ObservableMap<KeyCombination, Runnable>
var combos = scene.getAccelerators().keySet();
if (combos.stream().anyMatch(combo -> combo.match(event)) {
event.consume();
}
});
This latter option may cause issues if you're not careful. For instance, if you have a default Button in the Scene then the above event filter may interfere with the ENTER key. Also, this option won't necessarily stop things like shortcut+c, shortcut+v, etc. because those shortcuts are registered with the TextInputControl, not the Scene.

javafx CheckBoxTreeItem update parents programmatically

I have to retrieve some data from my database to dynamically create a TreeView and select some CheckBoxTreeItems from this TreeView. This TreeView represents permissions to a menu structure.
My doubt is when I create the TreeView and select specific items from the Tree according to the user's permissions programmatically, the parents items don't have any status change (selected or indeterminate). But when I select any item directly from the interface, the parents get updated.
For example, here I have my screen when I select the items programmatically:
You can see that I have two menu items selected, but the parents aren't.
On this image, I have selected the same menu items using the screen, and the parents were updated with indeterminate status or selected if I select all children inside the submenu.
I have gone through the documentation, google and here on Stack Overflow, but only found examples to update the children.
Is there a way to update the parents programmatically or to call the event executed from the screen when an item is selected?
EDIT:
All items from the Tree have the independent property set to false.
I came with a workaround for this problem.
I had to first create all the TreeView structure, and change the selected property after using this code snippet:
Platform.runLater(new Runnable() {
#Override
public void run() {
selectItems();
}
});
Here is the code to verify the TreeItems:
private void selectItems(){
TreeItem root = tree.getRoot();
if (root != null) {
selectChildren(root);
}
}
private void selectChildren(TreeItem<TesteVO> root){
for(TreeItem<TesteVO> child: root.getChildren()){
// HERE I CHECK IF THE USER HAS PERMISSION FOR THE MENU ITEM
// IF SO, I CHANGE THE SELECTED PROPERTY TO TRUE
if (child.getValue().id == 4) {
((CheckBoxTreeItem) child).setSelected(true);
}
// IF THERE ARE CHILD NODES, KEEP DIGGING RECURSIVELY
if(!child.getChildren().isEmpty()) {
selectChildren(child);
}
}
}
If there is a simpler way, please let me know!
This is not the case. Parent items do get automatically get set to the indeterminate state when you select a child item. I'm not sure if this is something that got corrected from the time that this question was posted, probably not.
My guess is that there's a programming bug in how the node was selected or how the TableView was constructed and initialized.
Here's some code that shows what I'm doing, and it works! In my case, I'm using a CheckBoxTreeItem<File> for the TreeItem.
How the treeview was created
treeView = new TreeView(root);
treeView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() {
#Override
public void changed(ObservableValue observableValue, Object o, Object t1) {
CheckBoxTreeItem<File> node = (CheckBoxTreeItem<File>)t1;
if (node.getValue() != currentFile) {
setFileDetail(node);
showChildren(node);
}
}
});
treeView.setCellFactory(new CallBackWrapper());
treeView.setShowRoot(false);
Below show the CallBackWrapper class.
private class CallBackWrapper implements Callback<TreeView<File>, TreeCell<File>> {
Callback<TreeView<File>, TreeCell<File>> theCallback;
private CallBackWrapper() {
theCallback = CheckBoxTreeCell.<File>forTreeView(getSelectedProperty, converter);
}
#Override
public TreeCell<File> call(TreeView<File> fileTreeView) {
return theCallback.call(fileTreeView);
}
final Callback<TreeItem<File>, ObservableValue<Boolean>> getSelectedProperty = (TreeItem<File> item) -> {
if (item instanceof CheckBoxTreeItem<?>) {
return ((CheckBoxTreeItem<?>) item).selectedProperty();
}
return null;
};
final StringConverter<TreeItem<File>> converter = new StringConverter<TreeItem<File>>() {
#Override
public String toString(TreeItem<File> object) {
File item = object.getValue();
return fileSystemView.getSystemDisplayName(item);
}
#Override
public TreeItem<File> fromString(String string) {
return new TreeItem<File>(new File(string));
}
};
}
And lastly here some code that the selection was made in:
boolean selectNode(CheckBoxTreeItem<File> parentNode, String name) {
Object[] children = parentNode.getChildren().toArray();
for (Object child : children) {
CheckBoxTreeItem<File> childItem = (CheckBoxTreeItem<File>) child;
if (name.equals(childItem.getValue().getName())) {
childItem.setSelected(true);
//treeView.getSelectionModel().select(child); <-- this does not work!
return true;
}
}
return false;
}

Create JavaFX ComboBox with custom Popup

I want to write a ComboBox with a custom Node object inside its Popup (rather than the common ListView). ColorPicker and DatePicker are good examples, which are the other two implementations of ComboBoxBase. I had thought I could easily extend ComboBoxBase, too, but since there is no popupProperty or popupFactory I don't know how to set the content. How else is it meant to be done? Or how ColorPicker and DatePicker do this?
ComboBoxPopupControl which extends ComboBoxBaseSkin contains getPopupContent(). That's the method you are looking for. In your own skin implementation, which extends one of the ComboBoxSkins, you can return the popup content you like (although it's not recommended to use private API)
public class CustomComboBox<T> extends ComboBox<T> {
#Override
protected Skin<?> createDefaultSkin() {
return new CustomComboBoxSkin<>(this);
}
}
public class CustomComboBoxSkin<T> extends ComboBoxPopupControl<T> {
public CustomComboBoxSkin(ComboBox<T> comboBox) {
super(comboBox, new CustomComboBoxBehaviour<>(comboBox));
}
#Override
public Node getPopupContent() {
return new Rectangle(150, 200);
}
// inherited methods ...
}
I used ComtextMenu to replace the commbox's popup like this:
ContextMenu menu = new ContextMenu();
MenuItem item = new MenuItem();
item.setGraphic(new Lable("test"));
menu.getItems.add(item);
commbox.setContextMenu(null);
commbox.setContextMenu(menu );
commbox.getContextMenu().show(comboBox, Side.BOTTOM, 0, 0);
It works fine.

afterburner.fx get view from node

i've been trying to get the grasp of afterburner.fx for few days now but i cant figure out this problem. please help
there are three tabs in a tab pane
tabPane.getTabs().get(0).setContent(new FirstView().getView());
tabPane.getTabs().get(1).setContent(new SecondView().getView());
tabPane.getTabs().get(2).setContent(new ThirdView().getView());
these are not named firstview, secondview etc. it's for demonstration...
now each of these views have a reload method:
firstView.reload()
secondView.reload()
thirdView.reload()
and i have setup a listener for tab changes so that i can reload these views once they come into view
tabPane.getSelectionModel().selectedItemProperty().addListener((o, oldValue, newValue) -> {
newValue.getView().reload(); // of course this cant be done like this
})
how to reload the view of the tab once it comes into view.?
You can add listeners to the individual tabs instead of to the tab pane:
FirstView firstView = new FirstView();
Tab tab0 = tabs.getTabs().get(0);
tab0.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
if (isSelected) {
firstView.reload();
}
});
tab0.setContent(firstView.getView());
// etc
i managed to find a work around for this. i made two new classes, one that extends FXMLView and another that extends Tab.
public abstract class ReloadableView extends FXMLView {
public abstract void reload();
}
and
public class ReloadingTab<V extends ReloadableView> extends Tab {
private V view;
public ReloadingTab(String text, V view) {
super(text, view.getView());
this.view = view;
}
public V getView() {
return view;
}
}
and finally added the listener
tabPane.getSelectionModel().selectedItemProperty().addListener((o, oldValue, newValue) -> {
ReloadingTab tab = (ReloadingTab) newValue;
tab.getView().reload();
});
sorry if my question didnt make any sense. i hope someone finds this useful.

Resources