Unfortunately, the old issue is still not fixed in fx9 nor later versions up and including fx18.
The original hack of the skin (see below the horizontal line) for fx9 seems to still be working in fx18. Though can be slightly improved since fx12 introduced access to the flow and tableHeaderRow.
While trying to adjust it to newer api, I came up with a - just as dirty and untested - approach. The idea is to override the actual layout method, that is the method that's called when laying out individual children of the table, watch out for the placeholder and replace it with the flow:
public class NoPlaceHolderSkin<T> extends TableViewSkin<T> {
public NoPlaceHolderSkin(TableView<T> control) {
super(control);
}
@Override
protected void layoutInArea(Node child, double areaX, double areaY, double areaWidth,
double areaHeight, double areaBaselineOffset, HPos halignment, VPos valignment) {
if (child.getStyleClass().contains("placeholder")) {
child.setVisible(false);
child = getVirtualFlow();
child.setVisible(true);
}
super.layoutInArea(child, areaX, areaY, areaWidth, areaHeight, areaBaselineOffset, halignment, valignment);
}
}
So revisited the hacks in the context of fx9. There had been changes, good and bad ones:
- Skins moved into a public package which now allows to subclass them without accessing hidden classes (good)
- the move introduced a bug which doesn't allow to install a custom VirtualFlow (fixed in fx10)
- reflective access to hidden members will be strongly disallowed (read: not possible) sometime in future
While digging, I noticed ever so slight glitches with the hacks (note: I did not run them against fx8, so these might be due differences in fx8 vs fx9!)
- the forced in-/visibility of placeholder/flow worked fine except when starting up with an empty table (placeholder was shown) and enlarging the table while empty (the "new" region looks empty)
- faking the itemCount to not-empty lets the rows dissappear on pressing navigation keys (which probably is not a big problem because users tend to not navigate an empty table) - this is definitely introduced in fx9, working fine in fx8
So I decided to go with the visibility enforcement: the reason for the slight glitches is that layoutChildren doesn't layout the flow if it thinks the placeholder is visible. That's handled by including the flow in the layout if super didn't.
The custom skin:
/**
* TableViewSkin that doesn't show the placeholder.
* The basic trick is keep the placeholder/flow in-/visible at all
* times (similar to https://stackoverflow.com/a/27543830/203657).
* <p>
*
* Updated for fx9 plus ensure to update the layout of the flow as
* needed.
*
* @author Jeanette Winzenburg, Berlin
*/
public class NoPlaceHolderTableViewSkin<T> extends TableViewSkin<T>{
private VirtualFlow<?> flowAlias;
private TableHeaderRow headerAlias;
private Parent placeholderRegionAlias;
private ChangeListener<Boolean> visibleListener = (src, ov, nv) -> visibleChanged(nv);
private ListChangeListener<Node> childrenListener = c -> childrenChanged(c);
/**
* Instantiates the skin.
* @param table the table to skin.
*/
public NoPlaceHolderTableViewSkin(TableView<T> table) {
super(table);
flowAlias = (VirtualFlow<?>) table.lookup(".virtual-flow");
headerAlias = (TableHeaderRow) table.lookup(".column-header-background");
// startet with a not-empty list, placeholder not yet instantiatet
// so add alistener to the children until it will be added
if (!installPlaceholderRegion(getChildren())) {
installChildrenListener();
}
}
/**
* Searches the given list for a Parent with style class "placeholder" and
* wires its visibility handling if found.
* @param addedSubList
* @return true if placeholder found and installed, false otherwise.
*/
protected boolean installPlaceholderRegion(
List<? extends Node> addedSubList) {
if (placeholderRegionAlias != null)
throw new IllegalStateException("placeholder must not be installed more than once");
List<Node> parents = addedSubList.stream()
.filter(e -> e.getStyleClass().contains("placeholder"))
.collect(Collectors.toList());
if (!parents.isEmpty()) {
placeholderRegionAlias = (Parent) parents.get(0);
placeholderRegionAlias.visibleProperty().addListener(visibleListener);
visibleChanged(true);
return true;
}
return false;
}
protected void visibleChanged(Boolean nv) {
if (nv) {
flowAlias.setVisible(true);
placeholderRegionAlias.setVisible(false);
}
}
/**
* Layout of flow unconditionally.
*
*/
protected void layoutFlow(double x, double y, double width,
double height) {
// super didn't layout the flow if empty- do it now
final double baselineOffset = getSkinnable().getLayoutBounds().getHeight() / 2;
double headerHeight = headerAlias.getHeight();
y += headerHeight;
double flowHeight = Math.floor(height - headerHeight);
layoutInArea(flowAlias, x, y,
width, flowHeight,
baselineOffset, HPos.CENTER, VPos.CENTER);
}
/**
* Returns a boolean indicating whether the flow should be layout.
* This implementation returns true if table is empty.
* @return
*/
protected boolean shouldLayoutFlow() {
return getItemCount() == 0;
}
/**
* {@inheritDoc} <p>
*
* Overridden to layout the flow always.
*/
@Override
protected void layoutChildren(double x, double y, double width,
double height) {
super.layoutChildren(x, y, width, height);
if (shouldLayoutFlow()) {
layoutFlow(x, y, width, height);
}
}
/**
* Listener callback from children modifications.
* Meant to find the placeholder when it is added.
* This implementation passes all added sublists to
* hasPlaceHolderRegion for search and install the
* placeholder. Removes itself as listener if installed.
*
* @param c the change
*/
protected void childrenChanged(Change<? extends Node> c) {
while (c.next()) {
if (c.wasAdded()) {
if (installPlaceholderRegion(c.getAddedSubList())) {
uninstallChildrenListener();
return;
}
}
}
}
/**
* Installs a ListChangeListener on the children which calls
* childrenChanged on receiving change notification.
*
*/
protected void installChildrenListener() {
getChildren().addListener(childrenListener);
}
/**
* Uninstalls a ListChangeListener on the children:
*/
protected void uninstallChildrenListener() {
getChildren().removeListener(childrenListener);
}
}
Usage example:
public class EmptyPlaceholdersInSkin extends Application {
private Parent createContent() {
// initially populated
//TableView<Person> table = new TableView<>(Person.persons()) {
// initially empty
TableView<Person> table = new TableView<>() {
@Override
protected Skin<?> createDefaultSkin() {
return new NoPlaceHolderTableViewSkin<>(this);
}
};
TableColumn<Person, String> first = new TableColumn<>("First Name");
first.setCellValueFactory(new PropertyValueFactory<>("firstName"));
table.getColumns().addAll(first);
Button clear = new Button("clear");
clear.setOnAction(e -> table.getItems().clear());
clear.disableProperty().bind(Bindings.isEmpty(table.getItems()));
Button fill = new Button("populate");
fill.setOnAction(e -> table.getItems().setAll(Person.persons()));
fill.disableProperty().bind(Bindings.isNotEmpty(table.getItems()));
BorderPane pane = new BorderPane(table);
pane.setBottom(new HBox(10, clear, fill));
return pane;
}
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
@SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(EmptyPlaceholdersInSkin.class.getName());
}