I am writting a little desktop application with a TreeView according to the Oracle-Example from here: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm.
From a MenuItem action of a ContextMenu, I would like to fire an event which shall create a new TreeItem below the item where I opened the ContextMenu from.
For MenuItem, it is possible to use the setOnAction(EventHandler<ActionEvent> event) method, but I only want to fire the action from a left mouse-click.
First, it is not possible to add an EventHandler to a MenuItem although it provides the method addEventHandler(EventType type, EventHandler<EventType> handler) with the event-type MouseEvent.ANY (or anything else). The handle-method of the event-handler is not called.
Second, i can use a workarround by adding a Label to a MenuItem by menuItem.setGraphic(label) and add an EventHandler to the label. This one works although MouseEvent.MOUSE_CLICKED is not called by an EventHandler's handle-method on a Label.
Is this "normal" behaviour? I understand that a label does not react on a click-event, but I do not understand why it is not possible to register a separate EventHandler or EventFilter on a MenuItem.
ContextMenu uses a MenuItemContainer, which is a
Container responsible for laying out a single row in the menu - in other
words, this contains and lays out a single MenuItem, regardless of it's
specific subtype.
Fur this purpose it seems to create new Nodes representing the MenuItem. So any EventHandlers added to the MenuItem will not be called.
To make it work as you intended, you can use a CustomMenuItem and add the according EventHandler to its content:
public class ContextMenuCell extends TreeCell<String> {
private ContextMenu menu;
public ContextMenuCell() {
Label lbl = new Label("Add item");
MenuItem menuItem = new CustomMenuItem(lbl);
lbl.setOnMouseClicked(evt -> {
if (evt.getButton() != MouseButton.PRIMARY) {
return;
}
TreeItem treeItem =
new TreeItem<String>("New item");
if (getTreeItem().isLeaf()) {
getTreeItem().getParent().getChildren().add(getIndex(), treeItem);
} else {
getTreeItem().getChildren().add(0, treeItem);
}
});
menu = new ContextMenu(menuItem);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item);
setGraphic(getTreeItem().getGraphic());
setContextMenu(menu);
}
}
}
Menu and MenuItem are not Nodes, so they will not handle mouse clicks since they are not displayed on the screen. A workaround is to set a graphics object (Node) to the MenuItem and add the listener to this Node. Works also for other menus like CheckMenuItem etc.:
public class RunJavaFX extends Application {
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
//the label will be our graphics object (Node)
Label l = new Label("Your Menu Text");
l.setTextFill(Color.BLACK); //set black since default CSS Style sets it to background color of the Menu
//add either over addEventFilter or addEventHandler
l.addEventFilter(MouseEvent.MOUSE_PRESSED, ev -> {
if (ev.getButton() == MouseButton.SECONDARY) {
System.out.println("RightClick: " + ev.getSource() + System.nanoTime());
} else {
System.out.println("Not Right Click: " + ev.getSource() + System.nanoTime());
}
ev.consume(); //optional
});
//create the MenuItem with an empty text and set the label l as graphics object
MenuItem mI = new MenuItem("", l);
//create the dummy menu and MenuBar for the example
Menu m = new Menu("Menu");
m.getItems().add(mI);
MenuBar mB = new MenuBar(m);
//create the dummy scene for the example
Scene scene = new Scene(mB);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Related
I'm using JFXListView and JFXListCell from the library called jfoenix and the purpose and function are same as the regular ListView.
The list contains some Label, Button and AnchorPane. At the very top and bottom of the list, I want to add non-selectable item. The item should not be selectable on mouse click, should not be able to focus and should not be able to scroll.
I though of using updateItem() function and setting the item disable:
#FXML
JFXListView listView;
ObservableList<AnchorPane> list = FXCollections.observableArrayList();
private void initializeListView(){
AnchorPane headerBottomPane = new AnchorPane();
headerBottomPane.setId("headerBottomPane");
....//some property of AnchorPane
list.add(headerBottomPane); //Add header AnchorPane
while(true){
AnchorPane listContainer = new AnchorPane();
Label title = new Label();
Label subtitle = new Label();
Button button = new Button();
Button button2 = new Button();
//Some code here...
listContainer.getChildren().addAll(label, subtitle, button, button2);
list.add(listContainer);
//some code here...
}
list.add(headerBottomPane); //Add bottom AnchorPane
listView.setCellFactory(new CallBack<JFXListView, JFXListCell>(){
#Override
public JFXListCell call(JFXListView param){
return new JFXListCell<AnchorPane>(){
#Override
protected void updateItem(AnchorPane anchorPane, boolean empty){
super.updateItem(anchorPane, empty);
if(anchorPane != null){
if(anchorPane.getId.equals("headerBottomPane")){
setDisable(true);
}
setItem(anchorPane);
}else{
setItem(null);
}
}
};
}
});
}
I am able to disable the top and last item of the list, the item is no longer able to select using mouseClick.
But the problem is, it is focusable when I use the Keyboard arrow up and arrow down another strange thing is when I use the mouse wheel to scroll the list, some of the item are becoming non-selectable too.
I would think of just using a VBox, and putting your top unselectable item first, then the ListView with all the selectable items, then the bottom unselectable item...
I think you have to put your listView.setCellFactory() function at the top of the code where you adding those items, try to initialize it before you add the item.
and inside your updateItem() try to use setMouseTransparent() and setFocusTravesable().
#Override
protected void updateItem(AnchorPane anchorPane, boolean empty){
super.updateItem(anchorPane, empty);
if(anchorPane != null){
if(anchorPane.getId.equals("headerBottomPane")){
setItem(anchorPane); //moved at the top
setMouseTransparent(true); //added this line
setFocusTraversable(false); //added this line
setDisable(true);
}else{
setItem(null);
}
}
I haven't test it but I hope it work.
Note: I am expanding on duplicate question here because it does not include a MCVE. The few other similar questions I've found also do not include working answers.
I am unable to find a way to have a ComboBox display the PromptText after clearing the selection.
Here is the MCVE:
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
final VBox root = new VBox(10);
root.setAlignment(Pos.TOP_CENTER);
root.setPadding(new Insets(10));
final ComboBox<String> cboSelection = new ComboBox<>();
final Button btnClear = new Button("Clear");
// Set ComboBox selections
final ObservableList<String> subjectsList = FXCollections.observableArrayList();
subjectsList.addAll("Software", "Math", "Physics");
// Setup the Subject selection
cboSelection.setPromptText("Select Subject");
cboSelection.setItems(subjectsList);
// Set action for "Clear" button
btnClear.setOnAction(e -> {
cboSelection.setValue(null);
});
root.getChildren().addAll(cboSelection, btnClear);
primaryStage.setTitle("ComboBox Demo");
primaryStage.setScene(new Scene(root, 200, 100));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Clicking the "Clear" button will set the selected value to null and clear the selection of the ComboBox, but the prompt text does not show again. This does not seem like the normal expected behavior.
I have tried clearSelection() as well as setPromptText() within the button's onAction and nothing seems to work to get the prompt text back.
According to the documentation, the prompt text should not actually be displayed at all here:
Prompt text is not displayed in all circumstances, it is dependent upon the subclasses of ComboBoxBase to clarify when promptText will be shown. For example, in most cases prompt text will never be shown when a combo box is non-editable (that is, prompt text is only shown when user input is allowed via text input).
If you want to see some prompt text when the selection is null (and you do not have an editable combo box), use a custom buttonCell on the combo box:
cboSelection.setPromptText("Select Subject");
cboSelection.setButtonCell(new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty) ;
if (empty || item == null) {
setText("Select Subject");
} else {
setText(item);
}
}
});
Note that it seems you also need to set the prompt text, as in the code in the question, in order to get the text to appear initially. I assume this is because of the same bug (I'm guessing that the library code is incorrectly setting the text of the button cell to the prompt text initially; if the prompt text is not set, the text gets set to null, apparently after the button cell's update method is invoked).
And you can obviously make this reusable by creating a named subclass of ListCell:
public class PromptButtonCell<T> extends ListCell<T> {
private final StringProperty promptText = new SimpleStringProperty();
public PromptButtonCell(String promptText) {
this.promptText.addListener((obs, oldText, newText) -> {
if (isEmpty() || getItem() == null) {
setText(newText);
}
});
setPromptText(promptText);
}
public StringProperty promptTextProperty() {
return promptText ;
}
public final String getPromptText() {
return promptTextProperty().get();
}
public final void setPromptText(String promptText) {
promptTextProperty().set(promptText);
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(getPromptText());
} else {
setText(item);
}
}
}
and then just
cboSelection.setButtonCell("Select Subject");
cboSelection.setButtonCell(new PromptButtonCell<>("Select Subject"));
Here is a code:
package tabpane;
import javafx.application.*;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.*;
public class HideShowTabContentOnClicked extends Application {
public static void main(String[] args) {
launch(args);
}
private BorderPane createContent() {
BorderPane borderPane = new BorderPane();
TabPane tabPane = new TabPane();
tabPane.setSide(Side.LEFT);
StackPane stackPane = new StackPane();
stackPane.setStyle("-fx-background-color: lightblue");
stackPane.setMinWidth(200);
Tab firstTab = new Tab("First");
firstTab.setClosable(false);
firstTab.setContent(stackPane);
firstTab.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
firstTab.setContent(null);
} else {
firstTab.setContent(stackPane);
}
});
Tab secondTab = new Tab("Second");
StackPane stackPane2 = new StackPane();
stackPane2.setStyle("-fx-background-color: yellow");
secondTab.setContent(stackPane2);
secondTab.setClosable(false);
secondTab.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
secondTab.setContent(null);
} else {
secondTab.setContent(stackPane2);
}
});
StackPane center = new StackPane();
center.setStyle("-fx-background-color: cyan");
borderPane.setCenter(center);
tabPane.getTabs().addAll(firstTab, secondTab);
borderPane.setLeft(tabPane);
return borderPane;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.setMaximized(true);
stage.show();
}
}
Here I tried to solve a problem by using selectedProperty() by setting content to null, but it doesn't working, I want to make Tab like toggle button so that when I click on it showed and hide TabPanes content.
Before
And when clicked
As an example I want to implement TabPane like Intellij IDEA Tool Buttons (like "Project", "Structure" Tool Buttons etc).
If you are going to keep your content into StackPane, you can bind stackPane.visibleProperty() with toggleButton.selectedProperty():
stackPane.visibleProperty()
.bind(Bindings.when(toggleButton.selectedProperty())
.then(false)
.otherwise(true)
);
in this exampl: toggleButton.isSelected() --> !stackPane.isVisible() and !toggleButton.isSelected() --> stackPane.isVisible(),
or listen ToggleButton's events:
// toggleButton.setOnAction(e ->{ //new .setOnAction() -> Override previous
toggleButton.addEventHandler(ActionEvent.ACTION, e ->{ //can add any quantity for your needs
if(toggleButton.isSelected())
stackPane.setVisible(false);
else stackPane.setVisible(true);
});
But the problem is instead of toggle button I want to use Tab, so that it behaves like toggle button. i.e. when click "First Tab" in my example code if content visible it should be invisible and vice versa. I mean only tabs should be shown
I found solution.Tab does not have click-handler... but
Tab tab = new Tab();
tab.setContent(stackPane);
Label lable = new Label("Label"); //create Label
tab.setGraphic(lable); //set Lable as Graphic to Tab
lable.setOnMouseClicked(event ->{ //setOnMouseClicked, for example
if(stackPane.isVisible()){
stackPane.setVisible(false);
}else{
stackPane.setVisible(true);
}
});
, you can use Label(for example) as Tab-text and add setOnMouseClicked()-handler to Label. You can use any Node with Handler/ActionListener -> It's up to you.
For example, you can use CheckBox to show/hide StackPane, and Tab text (you can combine FXML and Java-code to produce graphics):
Tab tab = new Tab("Tab2"); //Tab with Text
tab.setContent(stackPane);
CheckBox checkBox = new CheckBox(); //create CheckBox
tab.setGraphic(checkBox); //set CheckBox as Graphic to Tab
stackPane.visibleProperty()
.bind(Bindings.when(checkBox.selectedProperty())
.then(false)
.otherwise(true)
);
or
#FXML
private Tab tab;
// ...
tab.setGraphic(checkBox);
// ...
I have came up with this solution:
AtomicReference<Tab> currentTab = new AtomicReference<>(tabPane.getSelectionModel().getSelectedItem());
AtomicReference<Tab> lastTab = new AtomicReference<>(null);
tabPane.setOnMouseReleased(event -> {
// Check if current node is actually tab
Node n = event.getPickResult().getIntersectedNode();
while (n != null && !(n.getStyleClass().contains("headers-region"))) {
n = n.getParent();
}
if (n == null)
return;
lastTab.set(currentTab.get());
currentTab.set(tabPane.getSelectionModel().getSelectedItem());
if (currentTab.get() == lastTab.get()) {
// Hide
tabPane.setPrefSize(28, 28);
//tabPane.getSelectionModel().clearSelection(); // notify selection model
currentTab.set(null);
} else {
// Show
tabPane.setPrefSize(-1,-1);
currentTab.set(tabPane.getSelectionModel().getSelectedItem());
}
});
First of all, I have added mouse event to the tabPane. Inside this mouse event, check if node under cursor is actually Tab node. If it is, do some logic to identify what user is trying to do: hide or show. Hiding is a bit tricky, so I ended up with setting preferred size of TabPane to 28 px wide.
I have also tried to notify selection model with an empty newValue:
tabPane.getSelectionModel().clearSelection();
But this it is not working properly. Calling select(-1) should call clearSelection(), but behavior is different somehow.
When I select another tab after calling clearSelection(), selection model handler called with oldValue == null, that possibly does not update internal index and tab does not swithes to selected one.
In order for the end-user to constrain a search to some columns of the main TableView, I needed a treeview with checkboxes.
I decided to embed this TreeView in a popup, showing on click on a custom button.
I have created the following class, inspired from the question:
Java FX8 TreeView in a table cell
public class CustomTreeMenuButton extends MenuButton {
private PopupControl popup = new PopupControl();
private TreeView<? extends Object> tree;
private CustomTreeMenuButton me = this;
public void setTree(TreeView<? extends Object> tree) {
this.tree = tree;
}
public CustomTreeMenuButton() {
super();
this.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (!popup.isShowing()) {
Bounds b = me.localToScreen(me.getBoundsInLocal());
double x = b.getMinX();
double y = b.getMaxY();
popup.setAutoHide(true);
// popup.setAutoFix(true);
popup.setAnchorX(x);
popup.setAnchorY(y);
popup.setSkin(new Skin<Skinnable>() {
#Override
public void dispose() {
}
#Override
public Node getNode() {
return tree;
}
#Override
public Skinnable getSkinnable() {
return null;
}
});
popup.show(me.getScene().getWindow());
}
}
});
}
}
The tree I am working with contains CheckBoxTreeItem objects, and while the popup is working, there is some weird blur on all checkboxes, whenever the focus is not on a checkbox. (See GIF below)
First, I was thinking it was maybe an antialiasing problem, but popup.getScene().getAntiAliasing().toString() returns DISABLED
Then, I saw that non integer anchor points could cause problems. However popup.setAutoFix(true) did nothing, nor did the following:
popup.setAnchorX(new Double(x).intValue());
popup.setAnchorY(new Double(y).intValue());
It might be worth noting that I am working with FXML.
How can I get sharp checkboxes regardless of their focus ?
I would suggest a built-in control, CustomMenuItem, rather than reinventing the wheel:
A MenuItem that allows for arbitrary nodes to be embedded within it,
by assigning a Node to the content property.
An example
// Create the tree
CheckBoxTreeItem<String> rootItem = new CheckBoxTreeItem<String>("All stuff");
rootItem.setExpanded(true);
final TreeView<String> tree = new TreeView<String>(rootItem);
tree.setEditable(true);
tree.setCellFactory(CheckBoxTreeCell.<String>forTreeView());
for (int i = 0; i < 8; i++) {
final CheckBoxTreeItem<String> checkBoxTreeItem =
new CheckBoxTreeItem<String>("Stuff" + (i+1));
rootItem.getChildren().add(checkBoxTreeItem);
}
tree.setRoot(rootItem);
tree.setShowRoot(true);
// Create a custom menu item
CustomMenuItem customMenuItem = new CustomMenuItem(tree);
customMenuItem.setHideOnClick(false);
// Create the menu button
MenuButton mb = new MenuButton("Stuffs");
mb.getItems().add(customMenuItem);
And the output
Note: It is important to set the hideOnClickProperty to true, to avoid closing when the user clicks in the tree, which can be even done in the contructor, so you can shorten the initialization to:
CustomMenuItem customMenuItem = new CustomMenuItem(tree, false);
If you want to remove the hover glow, you can add the following CSS class:
.menu-item {
-fx-padding: 0;
}
I am creating TableView in JavaFX. In which I want to show Context Menu in right click of mouse in tableView. So I am adding an EventHandler on table as given below :
TableView tableView=new TableView();
EventHandler event = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent me) {
if (me.getButton() == MouseButton.SECONDARY) {
tableView.getContextMenu().show(tableView, me.getSceneX(), me.getSceneY());
}
}
};
tableView.addEventHandler(MouseEvent.MOUSE_CLICKED, event);
But my problem is that Context Menu is visible wherever I right click on any part of table.
I want to do that Context Menu should be only visible if I clicked on any rows in TableView.
i.e. How would I get row number in TableView at specific point, So that my Context Menu should be only visible, if I clicked on any row of TableView.
The best solution I found was to check if the y coordinate is outside of the bounds of the column header and then to explicitly show the menu.
ContextMenu visibleMenu = null;
tableView.setOnMouseClicked((MouseEvent e) -> {
if (visibleMenu !=null) {
visibleMenu.hide();
visibleMenu = null;
}
if (e.getButton()==MouseButton.SECONDARY) {
double columnHeaderHeight = tableView.lookup(".column-header-background").getBoundsInLocal().getHeight();
if (e.getY()>columnHeaderHeight) {
visibleMenu = getContextMenu(); // build on the fly or use a prebuild menu
visibleMenu.show(tableView, e.getScreenX(), e.getScreenY());
} else {
// you could show a header specific context menu here
}
}
});
The added benefit is that you can build the context menu on the fly with context sensitive items (that for example only appear if a certain type of cell is selected), or just reuse a prebuild contextmenu as setContextMenu does, up to you.
Add context menu to the specific cells using CellFactory not to the whole table.
E.g. using Table from Oracle tutorial:
TableColumn firstNameCol = new TableColumn();
firstNameCol.setText("First");
firstNameCol.setCellValueFactory(new PropertyValueFactory("firstName"));
firstNameCol.setCellFactory(new Callback<TableColumn, TableCell>() {
#Override
public TableCell call(final TableColumn param) {
final TableCell cell = new TableCell() {
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
if (isEditing()) {
setText(null);
} else {
setText(getItem().toString());
setGraphic(null);
}
}
}
};
// This way I will have context menu only for specific column
cell.setContextMenu(ContextMenuBuilder.create().items(MenuItemBuilder.create().text("menu").build()).build());
return cell;
}
});
may be the older question. There is a solution, like getting the target of the mouse event of the table and check for instance for class TableCellSkin and display the context menu as,
table.addEventHandler(MouseEvent.MOUSE_CLICKED,
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getButton() == MouseButton.SECONDARY
&& !isRowEmpty) {
EventTarget target = e.getTarget();
if (target instanceof TableCellSkin
|| ((Node) target).getParent() instanceof TableCellSkin) {
// do your stuff. Context menu will be displayed by default
} else {
// hide the context menu when click event is outside table row
table.getContextMenu().hide();
}
}
}
});
#FXML
void tableContextMenuRequested(ContextMenuEvent event) {
if (tableview.getSelectionModel().getSelectedItems().size() == 0) {
tableContextMenu.hide();
}
}