Reputation: 1945
In this minimal example we can drag the Tabs to reorder them:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.stage.Stage;
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
TabPane tabPane = new TabPane();
tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
Tab tab = new Tab("Tab");
tab.setGraphic(new Button("button"));
Tab tab2 = new Tab("Tab");
tab2.setGraphic(new Button("button"));
tabPane.getTabs().addAll(tab, tab2);
Scene scene = new Scene(tabPane);
tabPane.setPrefWidth(600);
tabPane.setPrefHeight(400);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
We can start a drag when the mouse is over the tabs name, but not when it is over the button (the tabs graphic). The button in this case is blocking the drag event from being seen by the tab.
I cannot simply set isMouseTransparant
as that prevents the button from registering its own mouse clicks.
How can I allow the user to drag a tab through the tabs graphic while still being able to interact with the graphic via clicking?
Upvotes: 3
Views: 143
Reputation: 159341
Example mode based solution
I suggest implementing a reorder mode.
When the reorder mode is switched on, the nodes in the graphics of the tab headers are all disabled and the user can click anywhere in the tab headers to drag and reorder the associated tabs. During this time, the user cannot interact with the disabled graphic nodes.
When the reorder mode is switched off, the nodes in the graphics of the tab headers are all enabled. The user cannot drag the tabs to reorder them, but they can interact with the nodes in the graphics to perform their functions.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.Objects;
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
TabPane tabPane = new TabPane();
Tab tab = new Tab("Tab");
tab.setGraphic(new Button("button"));
Tab tab2 = new Tab("Tab");
tab2.setGraphic(new Button("button"));
tabPane.getTabs().addAll(tab, tab2);
ToggleButton reorderModeToggle = new ToggleButton("Reorder Mode");
reorderModeToggle.selectedProperty().addListener((observable, wasSelected, isSelected) -> {
tabPane.getTabs().stream()
.map(Tab::getGraphic)
.filter(Objects::nonNull)
.forEach(graphic -> graphic.setDisable(isSelected));
tabPane.setTabDragPolicy(
isSelected
? TabPane.TabDragPolicy.REORDER
: TabPane.TabDragPolicy.FIXED
);
});
VBox layout = new VBox(10, reorderModeToggle, tabPane);
layout.setPadding(new Insets(10));
Scene scene = new Scene(layout);
tabPane.setPrefWidth(600);
tabPane.setPrefHeight(400);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
If new tabs can be added dynamically by the user, then you could additionally appropriately initialize their disable state based on the reorder mode (or you could do that in a list change listener for the tabs in the TabPane). I didn't provide logic for this additional optional extra step.
Discussion of why this approach is provided
I realize that is not what you originally asked for, but it might be a reasonable solution to your problem.
Trying to make it so that the graphics control work and the drag functions of the tab are also available at the same time to appear (to me) to be quite problematic, both in terms of how to implement this and the behavior that the user should expect to occur. For example, a button will be fired when the mouse is pressed, but a drag event for a tab header will also begin when a mouse is pressed, so, if you press a button in the header, should you also begin the drag event? It is quite unclear.
The input handling for buttons is handled by an InputMap class inside a ButtonBehavior reference by the ButtonSkin. Both of which are non-public APIs, so trying to override the default behavior is difficult. There are methods in the private InputMap class to help decide whether the behavior of the button should consume the event it takes actions on (a mouse press or key press) or not (by default it will). So this could be done by installing your own skin replicating (a lot) of the functionality in the private API with some modifications to provide the custom functionality you want (but I do not recommend this at all).
I had thought that using event filters you would be able to provide the original functionality you requested, and you may be able to do that, but from my investigations, that would not be at all easy.
Alternate Discussion
The alternate solution suggested by sorifiend in comments might also work for you:
... option is to not use a button. Try with a label instead (Labels don't capture any events by default), or you could use custom graphics to render the tab header to look however you want, and then it will behave exactly as you want without a button.
Alternately, instead of putting interactive graphics in the headers, you could place all your interactive nodes body of each tab, keeping only text or static images in the headers, allowing them to always be re-orderable.
Upvotes: 2