Expanding a bit (with some code) on my comment: as already noted, alignment (or in fx-speak: content display) of the sort indicator is not configurable, not in style nor by any property of column/header - instead, it's hard-coded in the header's layout code.
Meaning that we need to implement a custom columnHeader that supports configurable display. The meat is in a custom TableColumnHeader which has:
- a property
sortIconDisplayProperty()
to configure the relative location of the sort indicator
- an overridden
layoutChildren()
that positions the label and sort indicator as configured
- for fun: make that property styleable (needs some boilerplate around StyleableProperty and its registration with the CSS handlers)
To use, we need the whole stack of a custom TableViewSkin, TableHeaderRow, NestedTableColumnHeader: all just boiler-plate to create and return the custom xx instances in their relevant factory methods.
Below is an example which crudely (read: the layout is not perfect, should have some padding and guarantee not to overlap with the text ... but then, core isn't that good at it, neither) supports setting the icon left of the text. For complete support, you might want to implement setting it on top/bottom .. me being too lazy right now ;)
/**
* https://stackoverflow.com/q/49121560/203657
* position sort indicator at leading edge of column header
*
* @author Jeanette Winzenburg, Berlin
*/
public class TableHeaderLeadingSortArrow extends Application {
/**
* Custom TableColumnHeader that lays out the sort icon at its leading edge.
*/
public static class MyTableColumnHeader extends TableColumnHeader {
public MyTableColumnHeader(TableColumnBase column) {
super(column);
}
@Override
protected void layoutChildren() {
// call super to ensure that all children are created and installed
super.layoutChildren();
Node sortArrow = getSortArrow();
// no sort indicator, nothing to do
if (sortArrow == null || !sortArrow.isVisible()) return;
if (getSortIconDisplay() == ContentDisplay.RIGHT) return;
// re-arrange label and sort indicator
double sortWidth = sortArrow.prefWidth(-1);
double headerWidth = snapSizeX(getWidth()) - (snappedLeftInset() + snappedRightInset());
double headerHeight = getHeight() - (snappedTopInset() + snappedBottomInset());
// position sort indicator at leading edge
sortArrow.resize(sortWidth, sortArrow.prefHeight(-1));
positionInArea(sortArrow, snappedLeftInset(), snappedTopInset(),
sortWidth, headerHeight, 0, HPos.CENTER, VPos.CENTER);
// resize label to fill remaining space
getLabel().resizeRelocate(sortWidth, 0, headerWidth - sortWidth, getHeight());
}
// --------------- make sort icon location styleable
// use StyleablePropertyFactory to simplify styling-related code
private static final StyleablePropertyFactory<MyTableColumnHeader> FACTORY =
new StyleablePropertyFactory<>(TableColumnHeader.getClassCssMetaData());
// default value (strictly speaking: an implementation detail)
// PENDING: what about RtoL orientation? Is it handled correctly in
// core?
private static final ContentDisplay DEFAULT_SORT_ICON_DISPLAY = ContentDisplay.RIGHT;
private static CssMetaData<MyTableColumnHeader, ContentDisplay> CSS_SORT_ICON_DISPLAY =
FACTORY.createEnumCssMetaData(ContentDisplay.class,
"-fx-sort-icon-display",
header -> header.sortIconDisplayProperty(),
DEFAULT_SORT_ICON_DISPLAY);
// property with lazy instantiation
private StyleableObjectProperty<ContentDisplay> sortIconDisplay;
protected StyleableObjectProperty<ContentDisplay> sortIconDisplayProperty() {
if (sortIconDisplay == null) {
sortIconDisplay = new SimpleStyleableObjectProperty<>(
CSS_SORT_ICON_DISPLAY, this, "sortIconDisplay",
DEFAULT_SORT_ICON_DISPLAY);
}
return sortIconDisplay;
}
protected ContentDisplay getSortIconDisplay() {
return sortIconDisplay != null ? sortIconDisplay.get()
: DEFAULT_SORT_ICON_DISPLAY;
}
protected void setSortIconDisplay(ContentDisplay display) {
sortIconDisplayProperty().set(display);
}
/**
* Returnst the CssMetaData associated with this class, which may
* include the CssMetaData of its superclasses.
*
* @return the CssMetaData associated with this class, which may include
* the CssMetaData of its superclasses
*/
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return FACTORY.getCssMetaData();
}
/** {@inheritDoc} */
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
//-------- reflection acrobatics .. might use lookup and/or keeping aliases around
private Node getSortArrow() {
return (Node) FXUtils.invokeGetFieldValue(TableColumnHeader.class, this, "sortArrow");
}
private Label getLabel() {
return (Label) FXUtils.invokeGetFieldValue(TableColumnHeader.class, this, "label");
}
}
private Parent createContent() {
// instantiate the tableView with the custom default skin
TableView<Locale> table = new TableView<>(FXCollections.observableArrayList(
Locale.getAvailableLocales())) {
@Override
protected Skin<?> createDefaultSkin() {
return new MyTableViewSkin<>(this);
}
};
TableColumn<Locale, String> countryCode = new TableColumn<>("CountryCode");
countryCode.setCellValueFactory(new PropertyValueFactory<>("country"));
TableColumn<Locale, String> language = new TableColumn<>("Language");
language.setCellValueFactory(new PropertyValueFactory<>("language"));
TableColumn<Locale, String> variant = new TableColumn<>("Variant");
variant.setCellValueFactory(new PropertyValueFactory<>("variant"));
table.getColumns().addAll(countryCode, language, variant);
BorderPane pane = new BorderPane(table);
return pane;
}
/**
* Custom nested columnHeader, headerRow und skin only needed to
* inject the custom columnHeader in their factory methods.
*/
public static class MyNestedTableColumnHeader extends NestedTableColumnHeader {
public MyNestedTableColumnHeader(TableColumnBase column) {
super(column);
}
@Override
protected TableColumnHeader createTableColumnHeader(
TableColumnBase col) {
return col == null || col.getColumns().isEmpty() || col == getTableColumn() ?
new MyTableColumnHeader(col) :
new MyNestedTableColumnHeader(col);
}
}
public static class MyTableHeaderRow extends TableHeaderRow {
public MyTableHeaderRow(TableViewSkinBase tableSkin) {
super(tableSkin);
}
@Override
protected NestedTableColumnHeader createRootHeader() {
return new MyNestedTableColumnHeader(null);
}
}
public static class MyTableViewSkin<T> extends TableViewSkin<T> {
public MyTableViewSkin(TableView<T> table) {
super(table);
}
@Override
protected TableHeaderRow createTableHeaderRow() {
return new MyTableHeaderRow(this);
}
}
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
URL uri = getClass().getResource("columnheader.css");
stage.getScene().getStylesheets().add(uri.toExternalForm());
stage.setTitle(FXUtils.version());
stage.show();
}
public static void main(String[] args) {
launch(args);
}
@SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TableHeaderLeadingSortArrow.class.getName());
}
The columnheader.css to configure:
.column-header {
-fx-sort-icon-display: LEFT;
}
Version note:
the example is coded against fx9 - which moved Skins into public scope along with a bunch of other changes. To make it work with fx8
- adjust import statement to old locations in com.sun.** (not shown anyway, your IDE is your friend ;)
- for all SomethingHeader, change the constructors to contain the tableSkin as parameter and pass the skin in all factory methods (possible in fx8, as
getTableViewSkin()
- or similar - has protected scope and thus is accessible for subclasses)