In JavaFX 8 is was able to do the following to always center a selected row in a TableView in the middle of the viewport:
TableView<T> tv = getTableView();
// Position selection in the middle of the viewPort.
if (tv.getSelectionModel().getSelectedItem() != null) {
TableViewSkin<?> ts = (TableViewSkin<?>) tv.getSkin();
Optional<VirtualFlow> vfOpt = ts.getChildren().stream()
.filter(child -> child instanceof VirtualFlow)
.map(VirtualFlow.class::cast)
.findFirst();
// TODO sometimes not centering correctly. The scrollTo used in JavaFX 17
// differs from that used in JavaFX 8!
if (vfOpt.isPresent()) {
VirtualFlow vf = vfOpt.get();
int first = vf.getFirstVisibleCell().getIndex();
int last = vf.getLastVisibleCell().getIndex();
int selectedIndex = tv.getSelectionModel().getSelectedIndex();
int scrollPosition = selectedIndex - ((last - first) / 2) > 0 ? selectedIndex - ((last - first) / 2) : 0;
vf.scrollTo(scrollPosition);
}
}
In JavaFX 17, this no longer works. I tracked it down to the implementation of the vf.scrollTo(int) method, that has gone through some changes compared to JavaFX 8. The code above will sometimes work and sometimes it won't (depending on the first and last index).
I noted down the following (FI = first, LA = last, SEL = selectedIndex, POS = calculated scroll position, RES result):
FI = 0, LA = 16, SEL = 13, POS = 5, RES = to top of viewport
FI = 12, LA = 29, SEL = 13, POS = 5, RES = to middle of viewport
FI = 5, LA = 21, SEL = 13, POS = 5, RES = to top of viewport
So, it appears to have something to do with the calculated position falling already inside the viewport, causing the selected row to go to the top.
Can anyone offer any help?
VirtualFlow.scrollTo(int) only guarantees the item at the specified index will be visible in the viewport; it makes no guarantees about where in the viewport it will be positioned.
Instead, you can use VirtualFlow.scrollToTop(int) to move the selected item to the top of the viewport (if possible), and then use Viewport.scrollPixels(double) to adjust by half the height of the viewport. You need to layout the viewport in between (I think otherwise the second call overrides the first, though I am not entirely clear).
This approach should be more robust than your original approach, as it relies only on the specification, instead of the actual implementation, though I have not tested on versions prior to JavaFX 18.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
#Override
public void start(Stage stage) throws IOException {
TableView<Item> table = new TableView<>();
for (int i = 1 ; i <= 1000 ; i++) {
table.getItems().add(new Item("Item "+i));
}
TableColumn<Item, String> col = new TableColumn<>("Item");
col.setCellValueFactory(data -> data.getValue().nameProperty());
table.getColumns().add(col);
Button scrollSelected = new Button("Scroll to Selected");
scrollSelected.setOnAction(e -> {
int selected = table.getSelectionModel().getSelectedIndex();
if (selected == -1) return ;
TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();
skin.getChildren().stream()
.filter(VirtualFlow.class::isInstance)
.map(VirtualFlow.class::cast)
.findAny()
.ifPresent(vf -> {
vf.scrollToTop(selected);
vf.layout();
vf.scrollPixels(-vf.getHeight()/2);
});
});
HBox controls = new HBox(scrollSelected);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(5));
BorderPane root = new BorderPane(table);
root.setBottom(controls);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
public Item(String name) {
this.name.set(name);
}
public StringProperty nameProperty() {
return name ;
}
}
public static void main(String[] args) {
launch();
}
}
There are various other solutions, e.g.
vf.scrollTo(selected);
vf.layout();
Cell<?> cell = vf.getCell(selected);
double y = cell.getBoundsInParent().getCenterY();
vf.scrollPixels(y - vf.getHeight()/2);
Related
I am using PopOver from ControlsFX, in a TableView If I trigger the startEdit of a cell, it should pop the PopOver. This part it works, the problem is, the arrow which is pointing to the row is not on the right place every time. If I select a row from the table which is at the bottom of the table , it points to a cell above it.
I need that arrow to point every time to the right cell in the TableView.
ControlsFX , version: 8.40.14
How can I solve this?
Here is the code where you can see how it works:
package stackoverflow.popover;
import com.sun.deploy.util.StringUtils;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.controlsfx.control.PopOver;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
public class Controller implements Initializable {
#FXML
private TableView<Model> table;
#FXML
private TableColumn<Model, ObservableList<String>> listCell;
#Override
public void initialize(URL location, ResourceBundle resources) {
Model model = new Model(FXCollections.observableArrayList("Apple", "Peach"));
ObservableList<Model> items = FXCollections.observableArrayList();
for (int i = 0; i < 50; i++) {
items.add(model);
}
table.setItems(items);
table.setEditable(true);
listCell.setCellFactory(factory -> new ListTableCell(
FXCollections.observableArrayList("Apple", "Orange", "Peach", "Banana", "Lemon", "Lime")));
listCell.setCellValueFactory(data -> data.getValue().list);
}
private class ListTableCell extends TableCell<Model, ObservableList<String>> {
private ObservableList<String> allItems;
ListTableCell(ObservableList<String> allItems) {
this.allItems = allItems;
}
#Override
public void startEdit() {
super.startEdit();
PopOver popOver = new PopOver();
popOver.setAutoHide(true);
PopupController sc = new PopupController(allItems, new ArrayList<>(getItem()));
popOver.setContentNode(new StackPane(sc.getPane()));
popOver.setOnHiding(event -> commitEdit(sc.getItems()));
popOver.show(this);
}
#Override
protected void updateItem(ObservableList<String> item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(StringUtils.join(item, ","));
}
}
}
private class Model {
ListProperty<String> list;
public Model(ObservableList<String> list) {
this.list = new SimpleListProperty<>(list);
}
}
private class PopupController {
private BorderPane pane = new BorderPane();
private ListView<String> left = new ListView<>();
private ListView<String> right = new ListView<>();
private Button toLeft = new Button("<");
private Button toRight = new Button(">");
PopupController(List<String> all, List<String> selected) {
VBox leftBox = new VBox();
leftBox.setSpacing(5);
leftBox.getChildren().add(toRight);
leftBox.getChildren().add(left);
pane.setLeft(leftBox);
VBox rightBox = new VBox();
rightBox.setSpacing(5);
rightBox.getChildren().add(toLeft);
rightBox.getChildren().add(right);
pane.setRight(rightBox);
ObservableList<String> allItems = FXCollections.observableArrayList(all);
allItems.removeAll(selected);
left.setItems(allItems);
right.setItems(FXCollections.observableArrayList(selected));
toLeft.disableProperty().bind(right.getSelectionModel().selectedItemProperty().isNull());
toRight.disableProperty().bind(left.getSelectionModel().selectedItemProperty().isNull());
toLeft.setOnAction(event -> {
String str = right.getSelectionModel().getSelectedItem();
right.getItems().remove(str);
left.getItems().add(str);
});
toRight.setOnAction(event -> {
String str = left.getSelectionModel().getSelectedItem();
left.getItems().remove(str);
right.getItems().add(str);
});
}
BorderPane getPane() {
return pane;
}
ObservableList<String> getItems() {
return right.getItems();
}
}
}
Here are two screenshots to show what I mean :
This is even worst: (with setAutoFix(false))
I am not expert with ControlFX but I believe the problem you are facing its because the height of your PopOver is greater than your current screen size thus it is trying to relocate itself in a way to be inside the screen local bounds. So in order to achieve what you are trying you will need to manually set the ArrowLocation of your PopOver control. Here is how you can solve the issue (using your code) :
#Override
public void startEdit() {
super.startEdit();
PopOver popOver = new PopOver();
popOver.setAutoHide(true);
// first set auto fix to false
// to manually set the arrow location
popOver.setAutoFix(false);
PopupController sc = new PopupController(allItems, new ArrayList<>(getItem()));
// set a specific height for our pane
final double paneHeight = 300;
StackPane popOverPane = new StackPane(sc.getPane());
popOverPane.setPrefHeight(paneHeight);
popOver.setContentNode(popOverPane);
popOver.setOnHiding(event -> commitEdit(sc.getItems()));
// find coordinates relative to the screen
Bounds screenBounds = this.localToScreen(this.getBoundsInLocal());
// get our current y position ( on screen )
int yPos = (int) screenBounds.getMinY();
// get screen size
Rectangle2D primaryScreenBounds = Screen.getPrimary().getVisualBounds();
int screenHeight = (int) primaryScreenBounds.getHeight();
// if the PopOver height + the current position is greater than
// the max screen's height then set the arrow position to bottom left
if(screenHeight < yPos + paneHeight) {
popOver.setArrowLocation(ArrowLocation.LEFT_BOTTOM);
}
popOver.show(this);
}
Using the code above you would see some things you need to change and think more carefully.
The first one is that you will need to set a specific size for your StackPane or to find a dynamic way to calculate it.
Secondly in my example I am using the Screen.getPrimary() which will get the Rectangle2D dimensions of your primary screen and not the screen you have your application, this means that if you have more monitors with different resolution and your program is displayed on the second one, the code above will still use the first ( default ) monitor's resolution which might not match with the primary one, so you will have to find a way to get the correct monitor resolution.
Lastly you will need to do the same when the window is on the right side of the screen because then the width of the 'Popover' will exceed the width of your monitor
Try setting setAutoFix(false) on the PopOver instance. From the documentation of the autoFix property of the PopOver's superclass PopupWindow:
This convenience variable indicates whether, when the popup is shown,
it should automatically correct its position such that it doesn't end
up positioned off the screen.
I have an application written in JavaFX using a TreeView, the ContextMenu depends on the selected TreeItem, my TreeCell implementation uses setOnContextMenuRequested() to show the ContextMenu whenever I right click on a node, this works fine. I want now to achieve the same when navigating the tree with the keyboard and using the standard shortcut for the context menu on Windows (SHIFT+F10), I almost managed to do it, it displays the ContextMenu but when I try to use the navigation keys to select an item in the menu it displays the context menu of the main application windows and navigates in it, also I would like to display the context menu close to the selected cell, how can I do that? Below is a small application that exhibits my problem
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.PickResult;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application
{
#Override
public void start(Stage primaryStage)
{
try
{
BorderPane root = new BorderPane();
TreeView<String> treeView = new TreeView<>();
TreeItem<String> treeRoot = new TreeItem<>("Root");
for (int i = 1; i <= 5; i++)
{
TreeItem<String> child = new TreeItem<>("Item " + i);
child.getChildren().addAll(new TreeItem<>("Item " + i + "A"), new TreeItem<>("Item " + i + "B"));
treeRoot.getChildren().add(child);
}
treeView.setRoot(treeRoot);
root.setCenter(treeView);
ContextMenu contextMenu = new ContextMenu();
for (int i = 1; i <= 3; i++)
{
String text = "Choice " + i;
MenuItem menuItem = new MenuItem(text);
menuItem.setOnAction(event -> System.out.println(text));
contextMenu.getItems().add(menuItem);
}
treeView.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event ->
{
PickResult pickResult = event.getPickResult();
if (pickResult != null && pickResult.getIntersectedNode() != null)
{
TreeCell<String> cell = (TreeCell<String>) pickResult.getIntersectedNode();
Bounds bounds = cell.getBoundsInParent();
double x = bounds.getMinX() + (bounds.getWidth() / 2);
double y = bounds.getMinY() + (bounds.getHeight() / 2);
Point2D loc = cell.getParent().localToScreen(x, y);
contextMenu.show(cell, loc.getX(), loc.getY());
}
});
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
public static void main(String[] args)
{
launch(args);
}
}
KeenWrite's MarkdownEditor class may be doing something similar. Instead of a TreeCell the program uses a StyleClassedTextArea, but the idea should transfer to other widgets:
Create the context menu.
Associate the context menu with the widget
Add menu items to the context menu.
Get the display bounds for the widget.
Force focus when the menu is shown.
Show the menu.
In code, this resembles:
// The "text area" is analogous to the table cell widget.
final var textArea = getTextArea();
final var menu = new ContextMenu();
menu.setAutoHide( true );
menu.setHideOnEscape( true );
menu.setOnHidden( event -> textArea.setContextMenu( null ) );
final var items = menu.getItems();
textArea.setContextMenu( menu );
// Populate the menu items.
for( final var correction : suggestions ) {
items.add( createSuggestedItem( caretWord, correction ) );
}
// Figure out the location of the table cell, and call
// menu.show with those X and Y coordinates.
textArea.getCaretBounds().ifPresent(
bounds -> {
menu.setOnShown( event -> menu.requestFocus() );
menu.show( textArea, bounds.getCenterX(), bounds.getCenterY() );
}
);
You'll need to tweak the code. That works for my purposes: the context menu appears, the first item is selected, the arrow keys can change the selected item, and pressing Enter accepts the menu item.
For an application I'm developing I have a TreeView with (my own type of) TreeItems. This is working fine, and I get the items to display as expected.
I now want to be able to handle dragging an item from this TreeView to another part of the app window and have it perform some action there. I am now faced with two (at least…) issues:
Whenever you click in the TreeView, the item is always selected. Can this be prevented?
When adding a MouseEvent listener on the TreeView, I get the events with which I would be able to detect dragging and respond to that. I have, however not been able to determine the corresponding TreeItem for the mouse event. I need to know the exact TreeItem, of course, for the drag to work. Is this possible?
Some things I have tried:
I added my own cell factory and even when handling and consuming all mouse events on a cell, the item in the tree is still selected???
If I add a MouseEvent Handler to each and every cell, I will be able to manage the drag and drop, but given there could be thousands (potentially >> 100,000, not all expanded tough) of rows in the TreeView, isn't this a tremendous overhead and would it not be better to have just one event handler for the TreeView? (but then, how do I determine the corresponding TreeItem?)
The TreeView mouse events give me the following info:
No cell clicked: MouseEvent [source = TreeView[id=templateTreeView, styleClass=tree-view], target = TreeViewSkin$1#32a37c7a[styleClass=cell indexed-cell tree-cell]'null', eventType = MOUSE_PRESSED, consumed = false, x = 193.0, y = 289.0, z = 0.0, button = PRIMARY, primaryButtonDown, pickResult = PickResult [node = TreeViewSkin$1#32a37c7a[styleClass=cell indexed-cell tree-cell]'null', point = Point3D [x = 192.0, y = 8.0, z = 0.0], distance = 1492.820323027551]
Cell with text "Attributes" clicked: MouseEvent [source = TreeView[id=templateTreeView, styleClass=tree-view], target = TreeViewSkin$1#16aa9102[styleClass=cell indexed-cell tree-cell]'Attributes', eventType = MOUSE_PRESSED, consumed = false, x = 76.0, y = 34.0, z = 0.0, button = PRIMARY, primaryButtonDown, pickResult = PickResult [node = TreeViewSkin$1#16aa9102[styleClass=cell indexed-cell tree-cell]'Attributes', point = Point3D [x = 75.0, y = 13.0, z = 0.0], distance = 1492.820323027551]
I guess the secret is somewhere in the Node of the PickResult, but from there I'm still unable to see how to get to the TreeItem.
Hope there is an (easy) answer to this...
You are committing the sin of premature optimization :).
TreeCells are essentially only created for the currently visible items in a TreeView. When you expand or collapse nodes in the tree, or when you scroll, those TreeCells are reused to display different TreeItems. This is the purpose of the updateItem(...) and similar methods in TreeCell; they are called when the item displayed by that TreeCell instance changes.
A TreeCell on my system is about 1/4 inch high; to display 100,000 TreeCells would take a monitor more than 2,000 feet / 630 meters tall. At that point, you probably have more serious memory allocation issues than some extra listeners.... But at any rate, a listener would only be invoked if an event occurs on that particular cell, and occupies a fairly small footprint in comparison to the cell itself, so unless you have any direct evidence registering listeners on the cells (which as you've observed, massively reduces your code complexity) adversely affects performance, you should use the "listener per cell" approach.
Here is an example of a tree that holds 1,000,000 Integer-valued tree items. It tracks the number of TreeCells created (on my system it never seems to exceed 20 with the window size I set). It also displays a label; you can drag the values from the tree to the label and the label will display a running total of the values dropped there.
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TreeViewNoSelection extends Application {
private static int cellCount = 0 ;
private final DataFormat objectDataFormat = new DataFormat("application/x-java-serialized-object");
#Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>();
tree.setShowRoot(false);
Task<TreeItem<Integer>> buildTreeTask = new Task<TreeItem<Integer>>() {
#Override
protected TreeItem<Integer> call() throws Exception {
TreeItem<Integer> treeRoot = new TreeItem<>(0);
IntStream.range(1, 10).mapToObj(this::createItem)
.forEach(treeRoot.getChildren()::add);
return treeRoot ;
}
private TreeItem<Integer> createItem(int value) {
TreeItem<Integer> item = new TreeItem<>(value);
if (value < 100_000) {
for (int i = 0; i < 10; i++) {
item.getChildren().add(createItem(value * 10 + i));
}
}
return item ;
}
};
tree.setCellFactory(tv -> new TreeCell<Integer>() {
{
System.out.println("Cells created: "+(++cellCount));
setOnDragDetected(e -> {
if (! isEmpty()) {
Dragboard db = startDragAndDrop(TransferMode.COPY);
ClipboardContent cc = new ClipboardContent();
cc.put(objectDataFormat, getItem());
db.setContent(cc);
Label label = new Label(String.format("Add %,d", getItem()));
new Scene(label);
db.setDragView(label.snapshot(null, null));
}
});
}
#Override
public void updateItem(Integer value, boolean empty) {
super.updateItem(value, empty);
if (empty) {
setText(null);
} else {
setText(String.format("%,d", value));
}
}
});
IntegerProperty total = new SimpleIntegerProperty();
Label label = new Label();
label.textProperty().bind(total.asString("Total: %,d"));
label.setOnDragOver(e ->
e.acceptTransferModes(TransferMode.COPY));
// in real life use a CSS pseudoclass and external CSS file for the background:
label.setOnDragEntered(e -> label.setStyle("-fx-background-color: yellow;"));
label.setOnDragExited(e -> label.setStyle(""));
label.setOnDragDropped(e -> {
Dragboard db = e.getDragboard();
if (db.hasContent(objectDataFormat)) {
Integer value = (Integer) db.getContent(objectDataFormat);
total.set(total.get() + value);
e.setDropCompleted(true);
}
});
BorderPane.setMargin(label, new Insets(10));
label.setMaxWidth(Double.MAX_VALUE);
label.setAlignment(Pos.CENTER);
BorderPane root = new BorderPane(new Label("Loading..."));
buildTreeTask.setOnSucceeded(e -> {
tree.setRoot(buildTreeTask.getValue());
root.setCenter(tree);
root.setBottom(label);
});
primaryStage.setScene(new Scene(root, 250, 400));
primaryStage.show();
Thread t = new Thread(buildTreeTask);
t.setDaemon(true);
t.start();
}
public static void main(String[] args) {
launch(args);
}
}
For the selection issue: I would question why you want to do this; it would create an unusual user experience. The issue is probably that the "baked-in" event handlers which manage selection are being invoked before the handlers you define, so by the time you consume the event, selection has already been changed. You can try adding an event filter instead:
cell.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume);
but this will also disable expanding/collapsing the nodes in the tree.
So you can try something like:
cell.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
if (getTreeItem() != null) {
Object target = e.getTarget();
if (target instanceof Node && ((Node)target).getStyleClass().contains("arrow")) {
getTreeItem().setExpanded(! getTreeItem().isExpanded());
}
}
e.consume();
});
at which point it starts to look like something of a hack...
If you want to entirely disable selection, another option might be to create a custom selection model for the tree which just always returns an empty selection.
I have a table view with an anchor pane panel with 2 children, a table view and a pagination. The pagination is not directly linked to the table view (like if you put a button with a label that gets updated).
The only examples I found is that the pagination itself handles the UI updates via it's setPageFactory method.
I know I shouldn't design it like this, unfortunately I don't have the time to change it for now. So here's my current solution:
paginationTab1.setPageFactory(e -> {
updateTableViewWithOffset(e);
//hack, as the pagination is not directly linked with the tableView
//just return an empty component that is not managed by the parent component
Label l = new Label();
l.setManaged(false);
return l;
});
Is this an acceptable workaround (return null doesn't update well the UI after...) ? Or is there a way to get the same listener's behavior as the setPageFactory method provides (i.e get the page offset when it's clicked either on the pagination's arrows or pagination's numbers)?
You can observe the Pagination's currentPageIndexProperty():
paginationTab1.currentPageIndexProperty().addListener((obs, oldIndex, newIndex) ->
updateTableViewWithOffset(newIndex.intValue()));
Here's a SSCCE:
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.Scene;
import javafx.scene.control.Pagination;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class IndependentPaginationTest extends Application {
private static final int NUM_PAGES = 20 ;
private static final int ITEMS_PER_PAGE = 20 ;
#Override
public void start(Stage primaryStage) {
TableView<String> table = new TableView<>();
TableColumn<String, String> col = new TableColumn<>("Item");
table.getColumns().add(col);
col.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(cellData.getValue()));
updateTable(table, 0);
Pagination pagination = new Pagination();
pagination.setPageCount(NUM_PAGES);
pagination.currentPageIndexProperty().addListener((obs, oldIndex, newIndex) ->
updateTable(table, newIndex.intValue()));
BorderPane root = new BorderPane(table, null, null, pagination, null);
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.show();
}
private void updateTable(TableView<String> table, Integer index) {
int start = index * ITEMS_PER_PAGE + 1;
int end = start + ITEMS_PER_PAGE ;
table.getItems().setAll(
IntStream.range(start, end)
.mapToObj(Integer::toString)
.map("Item "::concat)
.collect(Collectors.toList()));
}
public static void main(String[] args) {
launch(args);
}
}
I've tried everything. I think they made a big mistake not giving any reference to the indexed cell in anything.
I can get my menu, but not in the right place. Right click is fine.
In my TreeView I can use get KeyReleased but I don't know where to put the menu.
setOnKeyReleased((KeyEvent t) -> {
switch (t.getCode()) {
case CONTEXT_MENU:
getSelectionModel().getSelectedItem().setGraphic(new Label("hi"));
//showMenu just calls show on my ContextMenu of my subclass TreeNode
((TreeNode)getSelectionModel().getSelectedItem()).showMenu(
getSelectionModel().getSelectedItem().getGraphic().getLocalToSceneTransform());
break;
}
});
None of the layout methods will give me the coords of the TreeCell
It simply isn't possible to provide API access to the cell for a given item. Not every item has a cell associated with it. On top of that, the item which is represented by a cell may change at any time, so even if you could provide access to the cell, the API would potentially be very confusing.
The basic trick to anything like this is to create a cell factory, and register the appropriate listeners with the cell. Your case is somewhat tricky, but possible. The following works to get the cell representing the selected item (you may want to modify the code somewhat to deal with the case where the cell is scrolled off the screen).
(Note that I used the Z key, arbitrarily, as I don't have a ContextMenu key on my laptop.)
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
TreeView<String> treeView = new TreeView<>();
TreeItem<String> treeRoot = new TreeItem<>("Root");
for (int i=1; i<=5; i++) {
TreeItem<String> child = new TreeItem<>("Item "+i);
child.getChildren().addAll(new TreeItem<>("Item "+i+"A"), new TreeItem<>("Item "+i+"B"));
treeRoot.getChildren().add(child);
}
treeView.setRoot(treeRoot);
root.setCenter(treeView);
ObjectProperty<TreeCell<String>> selectedCell = new SimpleObjectProperty<>();
treeView.setCellFactory(tree -> {
TreeCell<String> cell = new TreeCell<>();
cell.textProperty().bind(cell.itemProperty());
ChangeListener<TreeItem<String>> listener = (obs, oldItem, newItem) -> {
TreeItem<String> selectedItem = treeView.getSelectionModel().getSelectedItem();
if (selectedItem == null) {
selectedCell.set(null);
} else {
if (selectedItem == cell.getTreeItem()) {
selectedCell.set(cell);
}
}
};
cell.treeItemProperty().addListener(listener);
treeView.getSelectionModel().selectedItemProperty().addListener(listener);
return cell ;
});
ContextMenu contextMenu = new ContextMenu();
for (int i=1; i<=3; i++) {
String text = "Choice "+i;
MenuItem menuItem = new MenuItem(text);
menuItem.setOnAction(event -> System.out.println(text));
contextMenu.getItems().add(menuItem);
}
treeView.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.Z) {
if (selectedCell.get() != null) {
Node anchor = selectedCell.get();
// figure center of cell in screen coords:
Bounds anchorBounds = anchor.getBoundsInParent();
double x = anchorBounds.getMinX() + anchorBounds.getWidth() / 2 ;
double y = anchorBounds.getMinY() + anchorBounds.getHeight() / 2 ;
Point2D screenLoc = anchor.getParent().localToScreen(x, y);
contextMenu.show(selectedCell.get(), screenLoc.getX(), screenLoc.getY());
}
}
});
Scene scene = new Scene(root,400,400);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}