ContextMenu doesn't keep focus when using keyboard shortcut with JavaFX - java

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.

Related

JavaFX- center selected row in TableView

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);

ControlsFX: Ensure PopOver arrow always points to the right spot

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.

how to add remove button in combobox

I would like to create a ComboBox with the remove button like the picture below:
The picture uses Java Swing, and I don't know how to do this with JavaFX. I would like to create two ComboBoxes (a,b). When I click the "cross" in ComboBox a, I would like to remove a's item and add this item to ComboBox b, and ComboBox b so on.
ComboBox a:
(1)click item then remove it from a and add on b
ComboBox b:
(1)click item then do something(ex:print item)
(2)click cross then remove it from b and add on a
package UnitTest;
import Peer.Peer_Manager;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.chart.XYChart;
import javafx.geometry.Insets;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
public class temp extends Application {
final int height = 200;
final int weight = 300;
final int offset = 5;
Peer_Manager p_management;
XYChart.Series series_hop;
XYChart.Series series_gd;
#Override
public void start(Stage primaryStage) {
VBox vbox = new VBox();
vbox.setPadding(new Insets(5, 5, 5, 5));
vbox.setStyle("-fx-background-color: CORNSILK;");
Scene scene = new Scene(vbox, weight, height);
primaryStage.setScene(scene);
HBox hbBtn = new HBox();
Text t1=new Text(" A:");
Text t2=new Text(" B:");
String[] filename = {"A","B","C"};//conf.load_all();
ComboBox<String> cb = new ComboBox<String>();
cb.setItems(FXCollections.observableArrayList(filename));
cb.setVisibleRowCount(10);
ComboBox<String> cb2 = new ComboBox<String>();
cb.setVisibleRowCount(10);
vbox.getChildren().add(hbBtn);
hbBtn.getChildren().add(t1);
hbBtn.getChildren().add(cb);
hbBtn.getChildren().add(t2);
hbBtn.getChildren().add(cb2);
cb.setOnAction(e -> {
try {
Object object = cb.getValue();
if (object != null) {
cb2.getItems().add(object);
cb.getSelectionModel().clearSelection();
cb.getItems().remove(object);
}
} catch (Exception e1) {
e1.printStackTrace();
}
});
//would like to do something(ex:print item),but don't remove
//add the "cross" beside items,click "cross" to remove item and add on cb
cb2.setOnAction(e -> {
try {
Object object = cb2.getValue();
System.out.println(object);
if (object != null) {
cb1.getItems().add(object);
cb2.getSelectionModel().clearSelection();
cb2.getItems().remove(object);
}
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
});
primaryStage.setTitle("SimulatorFX");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The correct way is to use a CellFactory and create graphic nodes that contain the elements you wish to have. Here is an example:
public void start(Stage primaryStage) throws Exception {
ComboBox<String> cba = new ComboBox<>();
ComboBox<String> cbb = new ComboBox<>();
cba.getItems().addAll("A", "B", "C");
cbb.getItems().addAll("123", "456", "789");
// Set a cell factory for ComboBox A. A similar thing should be done for B.
cba.setCellFactory(lv ->
new ListCell<String>() {
// This is the node that will display the text and the cross.
// I chose a hyperlink, but you can change to button, image, etc.
private HBox graphic;
// this is the constructor for the anonymous class.
{
Label label = new Label();
// Bind the label text to the item property. If your ComboBox items are not Strings you should use a converter.
label.textProperty().bind(itemProperty());
// Set max width to infinity so the cross is all the way to the right.
label.setMaxWidth(Double.POSITIVE_INFINITY);
// We have to modify the hiding behavior of the ComboBox to allow clicking on the hyperlink,
// so we need to hide the ComboBox when the label is clicked (item selected).
label.setOnMouseClicked(event -> cba.hide());
Hyperlink cross = new Hyperlink("X");
cross.setVisited(true); // So it is black, and not blue.
cross.setOnAction(event ->
{
// Since the ListView reuses cells, we need to get the item first, before making changes.
String item = getItem();
System.out.println("Clicked cross on " + item);
if (isSelected()) {
// Not entirely sure if this is needed.
cba.getSelectionModel().select(null);
}
// Remove the item from A and add to B. You can add any additional logic in here.
cba.getItems().remove(item);
cbb.getItems().add(item);
}
);
// Arrange controls in a HBox, and set display to graphic only (the text is included in the graphic in this implementation).
graphic = new HBox(label, cross);
graphic.setHgrow(label, Priority.ALWAYS);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(graphic);
}
}
});
// We have to set a custom skin, otherwise the ComboBox disappears before the click on the Hyperlink is registered.
cba.setSkin(new ComboBoxListViewSkin<String>(cba) {
#Override
protected boolean isHideOnClickEnabled() {
return false;
}
});
VBox vb = new VBox(cba, cbb);
primaryStage.setScene(new Scene(vb));
primaryStage.show();
}

JavaFX: how to handle dragging an item from a TreeView

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.

Show context menu using keyboard for TreeCell

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);
}
}

Categories