2

This is a weird question so I'll make my best to explain myself properly.

What I'd like is to trigger an event when a Tab in a TabPane get clicked, and by "clicked" I mean just clicked, not necessarily selected.

I already tried using the selectedProperty of the Tab, but that does call the event only if the Tab is clicked when it's not selected, not even if it's clicked when it's already selected.

The reason why I'm doing this is that I'm trying to make a collapsible tab pane that hides the content of the TabPane if you click again on the opened tab, I've already wrote the code for collapsing the TabPane and that works but... I have no idea on how to get a click event from the tab header.

I've even looked into TabPane source code too hoping that I could find the tab header container but I didn't find it there.

kleopatra
  • 51,061
  • 28
  • 99
  • 211
Celeste Capece
  • 1,306
  • 2
  • 11
  • 20
  • 1
    the skin manages the nodes for the headers - it's convoluted, though, probably quite some work to change its behavior. Maybe you are better off to use a titledPane/accordion? btw, that's not specific to any particular version of fx (not much if any change between current 14 and 11), so removed the tag – kleopatra Jul 28 '20 at 11:30
  • Than you very much for the comment. I do not plan on changing the Skin, so it shouldn't be too much of a problem if I rely on some skin specific method/properties. I'll do some tests and see how it goes – Celeste Capece Jul 28 '20 at 11:58
  • @kleopatra It would be nice if `Tab` overrode `getStyleableNode()` and returned the instance the skin uses. It should be the responsibility of the skin to communicate the node somehow, possibly through the `Tab`'s [properties](https://openjfx.io/javadoc/14/javafx.controls/javafx/scene/control/Tab.html#getProperties()). Though to be fair, I don't really understand how the `Styleable` interface is supposed to work for classes which do not inherit from `Node`. – Slaw Jul 28 '20 at 12:19
  • @Slaw basically agree - but in the case of a Tab we would have an ambiguity: it has both header and content, so which to choose? – kleopatra Jul 28 '20 at 12:57
  • 2
    @kleopatra Personally, I'd choose the header. The content is already exposed by the `content` property. And it's really the header that "represents" the tab, at least in my opinion (which may be biased by how the default skin is implemented). – Slaw Jul 28 '20 at 13:02
  • Tried copying the whole TabPaneSkin and setting a pair of things to public and i can now access the headers with: `skin.tabHeaderArea.headersRegion.getChildren()`, i can even read the label of the tab header, but... using the `setOnMouseClicked` doesn't seem to do anything. Plus i've even tried to change the text of the labels and even if it changed in the debugger it didn't change in the GUI :S – Celeste Capece Jul 28 '20 at 13:10
  • @Slaw good point and would follow your lead :) Though not entirely certain if that wouldn't violate the contract of the styleable: probably should be a 1:1 to the complete representation. But don't know, just guessing - and the styleable implementations are half-hearted, to put it mildly ;) – kleopatra Jul 28 '20 at 13:23
  • Quick and dirty approach is to set the graphic of the tab to a label (leaving the text blank); register a mouse handler with the label. – James_D Jul 28 '20 at 13:31

1 Answers1

4

No need for a completely new skin - we can access the header nodes by lookup. Beware: implies relying on implementation details, which might change across versions.

The (undocumented!) style id to look for is ".tab-container" - that's the only child of the TabHeaderSkin (== region for a single tab header). It contains the label, the close button (if any) and the focus marker. This "skin" keeps a reference to its tab in its properties (undocumented, of course ;)

So the basic approach is to

  • lookup all tab-containers after the skin is installed
  • for each, register an appropriate mouse handler on its parent
  • on the respective mouseEvent, do whatever is needed

Note that the listeners have to be removed/added when the list of tabs is modified (not included in the snippet below).

In example code:

/** 
 * looks up the styled part of tab header and installs a mouseHandler
 * which calls the work load method.
 * 
 * @param tabPane
 */
private void installTabHandlers(TabPane tabPane) {
    Set<Node> headers = tabPane.lookupAll(".tab-container");
    headers.forEach(node -> {
        // implementation detail: header of tabContainer is the TabHeaderSkin
        Parent parent = node.getParent();
        parent.setOnMouseClicked(ev -> handleHeader(parent));
    });
}

/**
 * Workload for tab.
 * @param tabHeaderSkin
 */
private void handleHeader(Node tabHeaderSkin) {
    // implementation detail: skin keeps reference to associated Tab
    Tab tab = (Tab) tabHeaderSkin.getProperties().get(Tab.class);
    System.out.println("do stuff for tab: " + tab.getText());
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • just tried it on an empty application with only a tabPane, and... worked perfectly! Still gotta figure out why it doesn't on my real application but... guess that's my problem. Thank you very much for the help! – Celeste Capece Jul 28 '20 at 16:20