Search code examples
javajavafx

How to make TableView always add specific column for sorting in JavaFX?


I need to make table always sort by specific column (when some settings are enabled). For example when user sorts a table by a column he wants, program will automatically add another column for sorting.

This is my code - program must always sort by ID ASC:

public class JavaFxTest7 extends Application {

    private static record Student(int id, String name, int mark) {};

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        var table = this.createTable();
        table.getSortOrder().addListener((ListChangeListener<? super TableColumn<Student, ?>>)(change) -> {
            //if some settings enabled then
            var idColumn = table.getColumns().get(0);
            if (!table.getSortOrder().contains(idColumn)) {
                idColumn.setSortType(TableColumn.SortType.ASCENDING);
                table.getSortOrder().add(0, idColumn);
            }
        });
        var scene = new Scene(table, 300, 200);
        stage.setScene(scene);
        stage.show();
    }

    private TableView<Student> createTable() {
        var table = new TableView<Student>();
        table.getItems().addAll(new Student(1, "Billy", 3),
                new Student(2, "Johnny", 4), new Student(3, "Mickey", 5));
        var idColumn = new TableColumn<Student, Integer>("ID");
        idColumn.setCellValueFactory((data) ->  new ReadOnlyObjectWrapper<>(data.getValue().id()));
        var nameColumn = new TableColumn<Student, String>("Name");
        nameColumn.setCellValueFactory((data) ->  new ReadOnlyStringWrapper(data.getValue().name()));
        var markColumn = new TableColumn<Student, Integer>("Mark");
        markColumn.setCellValueFactory((data) ->  new ReadOnlyObjectWrapper<>(data.getValue().mark()));
        table.getColumns().addAll(idColumn, nameColumn, markColumn);
        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
        return table;
    }
}

And this is the result:

enter image description here

Sorting by ID is always added as planned. The problem is that with my solution it is not possible to remove sorting by other columns - it doesn't matter how many times you click on name column, name column always exists in sortOrder list.

Could anyone say how to fix it?


Solution

  • The behavior you're seeing seems to be as designed. If the user clicks on a column without shift down, then the user is requesting that the table reset the sort order so that it only consists of the clicked-on column. So, you're clicking on the "Name" column expecting it to be removed from the sort order, but actually you're requesting all other columns be removed from the sort order. Then because your listener immediately re-adds the "ID" column, it looks like something went wrong.

    That said, if you take into account the aforementioned behavior in the listener then implementing what you want appears to be possible (at least in JavaFX 22). Here is an example. Note, however, that due to the (necessary) use of runLater, the contents of the column headers sometimes flicker when changing the sort order.

    package com.example;
    
    import java.util.ArrayList;
    import java.util.List;
    import javafx.application.Platform;
    import javafx.collections.ListChangeListener;
    import javafx.collections.ObservableList;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableColumn.SortType;
    import javafx.scene.control.TableView;
    
    public class PrefixedSortOrder<S> {
    
      /**
       * Makes it so the {@link TableView#getSortOrder() sortOrder} of {@code table} always contains the
       * columns of {@code prefixCols}, in order, as a prefix. This constraint is maintained whether the
       * {@code sortOrder} is changed programmatically or via user interaction.
       *
       * @param <S> The item type of the {@code table}.
       * @param table the {@code TableView}
       * @param prefixCols the columns to always have at the start of the {@code sortOrder}
       */
      public static <S> void install(TableView<S> table, List<TableColumn<S, ?>> prefixCols) {
        var pso = new PrefixedSortOrder<>(table, prefixCols);
        table.getSortOrder().addListener(pso::onChanged);
      }
    
      private final ObservableList<TableColumn<S, ?>> sortOrder;
      private final List<TableColumn<S, ?>> prevSortOrder;
      private final List<TableColumn<S, ?>> prefixCols;
      private boolean fixingSortOrder;
    
      private PrefixedSortOrder(TableView<S> table, List<TableColumn<S, ?>> prefixCols) {
        this.sortOrder = table.getSortOrder();
        this.prevSortOrder = new ArrayList<>(sortOrder);
        this.prefixCols = List.copyOf(prefixCols);
      }
    
      private void onChanged(ListChangeListener.Change<? extends TableColumn<S, ?>> c) {
        if (fixingSortOrder) return;
    
        if (isPrefixMissing()) {
          // Sort order changed in manner consistent with a simple click.
          fixingSortOrder = true;
          // Avoid modifying the list from within the listener.
          Platform.runLater(
              () -> {
                fixSortOrder();
                cacheSortOrder();
                fixingSortOrder = false;
              });
        } else {
          // Sort order changed in manner consistent with a shift+click.
          cacheSortOrder();
        }
      }
    
      private boolean isPrefixMissing() {
        if (sortOrder.size() < prefixCols.size()) {
          return true;
        }
    
        for (int i = 0; i < prefixCols.size(); i++) {
          if (prefixCols.get(i) != sortOrder.get(i)) {
            return true;
          }
        }
        return false;
      }
    
      private void fixSortOrder() {
        if (sortOrder.size() == 1) {
          // Sort order changed via user interaction (probably).
          var column = sortOrder.get(0);
          if (prefixCols.contains(column)) {
            // User toggled sort direction of prefix column.
            sortOrder.setAll(prevSortOrder);
          } else if (wasSortOrderCleared(column)) {
            // User removed non-prefix column from sort order.
            sortOrder.setAll(prefixCols);
          } else {
            // User toggled sort direction of non-prefix column.
            sortOrder.addAll(0, prefixCols);
          }
        } else {
          // Sort order changed programmatically (probably).
          var temp = new ArrayList<>(sortOrder);
          temp.removeAll(prefixCols);
          temp.addAll(0, prefixCols);
          sortOrder.setAll(temp);
        }
      }
    
      private boolean wasSortOrderCleared(TableColumn<S, ?> column) {
        return column.getSortType() == SortType.ASCENDING && prevSortOrder.contains(column);
      }
    
      private void cacheSortOrder() {
        prevSortOrder.clear();
        prevSortOrder.addAll(sortOrder);
      }
    }