JavaFX TreeView: remove expand / collapse button (disclosure node) & functionality - javafx

I want to have a TreeView that has all of its children permanently expanded, and I don't want the user to be able to expand or collapse any of the children.
To do this I've found that I need to do the following:
Remove icon with CSS (Done)
Change expand and collapse image TreeView JavaFX 2.2
[edit] Above link should be used to change image; to remove completely, use this solution: https://stackoverflow.com/a/27831191/4430591
Remove double click functionality (Done)
Disable TreeItem's default expand/collapse on double click JavaFX 2.2
[edit] Remove ability to collapse / expand using keyboard arrrow keys (Done)
Given in José Pereda's solution below ( https://stackoverflow.com/a/27831085/4430591 )
[edit] Remove ability to right click for a ContextMenu (Done)
Given in José Pereda's solution below ( https://stackoverflow.com/a/27831085/4430591 )
Remove icon's clickablity (How do I do this?)
[edit] solution: https://stackoverflow.com/a/27831191/4430591
Even though the icon is no longer visible, it's still clickable. I don't see any way of filtering this; I only see ways to be able to respond to it after the fact.
Also, if I'm missing anything else that I need to do to ensure this functionality, please let me know.

I feel quite silly. I think this was mostly just a matter of not knowing what that darn arrow was called. Apparently it's a disclosureNode? Maybe that's common knowledge.
In the custom defined TreeCell, all I did was add this line in the updateItem method:
setDisclosureNode(null);

The solution to avoid modifying the skin or the default behavior is more simple if we trap the clicks before they are dispatched, and consume the right ones.
For that we can use an EventDispatcher, to filter both the mouse pressed and the right click over the arrows, which are StackPane nodes:
class CellEventDispatcher implements EventDispatcher {
private final EventDispatcher original;
public CellEventDispatcher(EventDispatcher original) {
this.original = original;
}
#Override
public Event dispatchEvent(Event event, EventDispatchChain tail) {
if (event.getEventType().equals(MouseEvent.MOUSE_PRESSED) ||
event.getEventType().equals(ContextMenuEvent.ANY)){
event.consume();
}
if(event instanceof KeyEvent && event.getEventType().equals(KeyEvent.KEY_PRESSED)){
if((((KeyEvent)event).getCode().equals(KeyCode.LEFT) ||
((KeyEvent)event).getCode().equals(KeyCode.RIGHT))){
event.consume();
}
}
return original.dispatchEvent(event, tail);
}
}
Now we apply our custom dispatcher to the tree view:
#Override
public void start(Stage primaryStage) {
TreeView<String> tree = new TreeView<>();
...
EventDispatcher treeOriginal = tree.getEventDispatcher();
tree.setEventDispatcher(new CellEventDispatcher(treeOriginal));
Scene scene = new Scene(tree);
primaryStage.setScene(scene);
primaryStage.show();
}
This will consume any click (left or right) over the arrows on the tree.
EDIT
Added to the event dispatcher class the case where the user uses the keyboard to traverse the tree view, consuming the collapse/expand events with arrow LEFT or RIGHT.

Related

JavaFX ContextMenu max size has no effect

If a ContextMenu has lots of items, it fills the entire screen. It seems that ContextMenu.setMaxSize has no effect whatsoever.
Is there a way to restrict the size of a ContextMenu, in a way that it is still scrollable via mouse wheel & and the up and down buttons appear?
I guess I could roll my own control with VBox & Scrollpane, but I'd like to avoid this if possible.
Unfortunately, limiting the size of popup is not supported: the Region that's responsible for showing the MenuItems is ContextMenuContent and implements its computeMaxHeight to return the screenHeight. That container is created by ContextMenuSkin and stored into a private final field, so there's no way to replace it with a custom implementation with a more intelligent implementation.
What we can do, though, is to access that region and set its maxHeight to the same value as the ContextMenu. To remain off the evil illegal reflective access to the private field, we can register a handler for the Menu.ON_SHOWING event and update the size as needed [*].
Something like
public class MaxSizedContextMenu extends ContextMenu {
public MaxSizedContextMenu() {
addEventHandler(Menu.ON_SHOWING, e -> {
Node content = getSkin().getNode();
if (content instanceof Region) {
((Region) content).setMaxHeight(getMaxHeight());
}
});
}
}
[*] update: to make this work, the ContextMenu must have a reasonable maxHeight (default is Double.MAX_VALUE), that is it must be set manually after instantiation. Furthermore, we have to use the ContextMenu's maxHeight in the eventHandler (vs. f.i. an arbitrary constant), otherwise vertical location of the popup is broken - the layout code still thinking, that it's filling the entire screen height.
ContextMenu menu = new MaxSizedContextMenu();
menu.setMaxHeight(200);

JavaFX wait for enter

Essentially, what I'm trying to do is something like to a text-based RPG using JavaFX. Right now, to display some text, I've got this:
final IntegerProperty i = new SimpleIntegerProperty(0);
Timeline timeline = new Timeline();
KeyFrame keyFrame = new KeyFrame(
Duration.millis(70),
event -> {
if (i.get() > info.getText().length()) {
timeline.stop();
} else {
text.setText(info.getText().substring(0, i.get()));
i.set(i.get() + 1);
}
});
timeline.getKeyFrames().add(keyFrame);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
timeline.setOnFinished(a -> {
hb_start.getChildren().clear();
hb_start.getChildren().addAll(start_left,start_right);
hb_start.setAlignment(Pos.CENTER);
});
Because the length of the animation depends on the size of the text, the cyclecount is set to indefinite. Unless there's some other way I'm missing to make the animation play once and then stop, I'd like it so that when you press enter (or some other key that I decide on later) for it to call timeline.stop(); but I can't figure out how to add any sort of listener. Trying to implement keyListenerseems to come with all sorts of stuff that I don't need, and it also doesn't work with a TextField, and instead wants a JTextField, which might be fine, except that I don't have a clue how to do anything with Swing.
Currently, the text is being displayed in aTextFlow from the text of Text. I'm assuming the listener would be added to the TextFlow, or even the scene itself, honestly, I'm at a loss for what to do. It sounds simple, but I can't seem to figure it out.
KeyListener is a AWT class, not a JavaFX class. Unless you're embedding a Swing component in your JavaFX application or a JavaFX node in a Swing application, you should use JavaFX's equivalent EventHandler<KeyEvent> instead. Furthermore there is no need to include a TextField (or Swing's JTextField) in your application just for the sake of receiving key events. You could add the listener directly to the Scene:
final KeyCode stopKey = KeyCode.ENTER;
EventHandler<KeyEvent> handler = event -> {
if (event.getCode() == stopKey) {
timeline.stop();
}
};
scene.setOnKeyPressed(handler);
Note that events can be consumed by nodes before they reach the scene, e.g. by a TextField that has the focus. In this case you could make sure you get the event by registering a the listener as a event filter instead:
// scene.setOnKeyPressed(handler);
scene.addEventFilter(KeyEvent.KEY_PRESSED, handler);

Prevent a toggle group from not having a toggle selected - Java FX

I have a toggle group with two toggle buttons that should look like this (Yellow toggle is the selected toggle).
However when I click on the selected toggle both toggles become unselected and look like this.
Then if I try to get whether the toggle is selected, I get a nullPointerException.
(Boolean) toggleGroup.getSelectedToggle().getUserData();
Is it possible to prevent the selected toggle from being unselected when it is clicked?
This one works for me. If the new selected element is null that means theres no selected element, therefore just select the previous one (which is the "oldValue")
toggleGroup.selectedToggleProperty().addListener((obsVal, oldVal, newVal) -> {
if (newVal == null)
oldVal.setSelected(true);
});
You can try the following code to create a persistent toggle.
/**
* Create a toggle group of buttons where one toggle will always remain switched on.
*/
class PersistentButtonToggleGroup extends ToggleGroup {
PersistentButtonToggleGroup() {
super();
getToggles().addListener(new ListChangeListener<Toggle>() {
#Override public void onChanged(Change<? extends Toggle> c) {
while (c.next()) {
for (final Toggle addedToggle : c.getAddedSubList()) {
((ToggleButton) addedToggle).addEventFilter(MouseEvent.MOUSE_RELEASED, new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (addedToggle.equals(getSelectedToggle())) mouseEvent.consume();
}
});
}
}
}
});
}
}
This just reacts to mouse events, so perhaps not ideal if you want to account for keyboard events or changing toggles in code.
A similar, but perhaps more complete approach that I haven't tried may be defined in the blog entry Button of Choice: Use ToggleButtons as RadioButtons.
Perhaps the simplest behavior to accomplish what you desire might be to just use RadioButtons instead of ToggleButtons.
If, additionally, you want the buttons styled like ToggleButtons, instead of RadioButtons, then you can try the styling technique which is outlined in: How to make a RadioButton look like regular Button in JavaFX.
RadioButton radioButton=new RadioButton("Radio");
radioButton.getStyleClass().remove("radio-button");
radioButton.getStyleClass().add("toggle-button");

How to skip DRAG_DETECTED Events from TableHeaderRow (resizing TableColumns)

In order to use a TableView (with resizable columns) as drag source,
I have attached an onDragDetected handler on that TableView with the effect, that resizing TableColumns does not work anymore: If the user clicks into the TableHeaderRow for dragging the column separators, these mouse events are consumed by my handler too.
The handler is attached to the whole TableView and I do not see a way to distinguish between events from TableRows and events from the TableHeaderRow.
Attaching the handler to the data-rows only is not appropriate in my case, since I need multiline selection (and do not want to have dependencies from the RowFactory to the application data model).
Any suggestions?
best Hans
I found the solution myself: I had to look up the TableHeaderRow instance with the lookup-method, and discard the DRAG_DETECTED event there.
Pane header = (Pane) mainTableView.lookup("TableHeaderRow");
header.setOnDragDetected(new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
System.out.println("ignoring drag operation from table header.");
event.consume();
}
});
After that, resising of table columns works. Maybe there is something wrong with my drag+drop event handling that initially caused the problem!? Somebody voted down without comment...
The solution by Hans does work, but only after the window is shown. I added a window.setOnShown((WindowEvent e) listener and then I was able to do the lookup and header.setOnDragDetected.
Here is how I added it:
window.setOnShown((WindowEvent e) -> {
Pane header = (Pane) mainTableView.lookup("TableHeaderRow");
....
});
Otherwise, lookup returns null.

javafx setFocus after tabPaine change

Problem:
Have tabPane tabs OK.
In the first tab there is a text field. I am able to get focus on this field when starting the application.
After changing the tabs and coming back to the first tab I want focus to be on this textfield (barcodereader should be active in this field) without having to select the field with the mouse.
I am able to catch event from tabs with
tp.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>()
{ etc
(could not post with code)
and I am able to trigger en event for the first tab.
But field.requestFocus(); does not work. Probably because this method comes before rendering the textfield.
So here is my question:
How do you set focus on a control after clicking tabs in TabPane?
If you handle the mouse release event, it works: (The doFocus enables the requestFocus handling only when a tab selection changed before, otherwise it kicks in every time you click somewhere in the TabPane.)
final SimpleBooleanProperty doFocus = new SimpleBooleanProperty(false);
tabPane.setOnMouseReleased(new EventHandler<Event>() {
#Override
public void handle(Event event) {
if (!doFocus.get()) {
return;
}
doFocus.set(false);
switch (tabPane.selectionModelProperty().getValue().selectedIndexProperty().intValue()) {
case 0: tf1b.requestFocus(); break;
case 1: tf2a.requestFocus(); break;
default: break;
}
}
});
tabPane.selectionModelProperty().getValue().selectedIndexProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
doFocus.set(true);
}
});
When the TabPane has focus, one can change tab selection with the cursor keys and there the TextFields also won't get the focus with selection based approach. This probably should be handled too, if you need it.
(Recently I had a similar problem. I noticed, that the TabPane switches tabs immediately when you press the mouse button. My guess would be, that the selection based approach requests focus on the TextField right after mouse down, but the continued mouse down steals the focus back to the TabPane. Or maybe even the single mouse down event which changes selection causes the focus to go back to TabPane. However, my assumptions regarding the reasons may not be correct, as I am a newbie to JavaFX.)
EDIT: That handling certainly is not optimal. For instance, if you change tabs with the keys, the doFocus will be enabled and then clicking anywhere in the TabPane will trigger the requestFocus call. I thought this should be mentioned.
Also, take a look at my solution for setting focus on TextArea, when user changes selected tab(using mouse or keyboard) https://stackoverflow.com/a/19046535/2791746

Resources