From abf65b1cc23cbece4cebed4f976afbb69321ba09 Mon Sep 17 00:00:00 2001
From: 0xb00bface <0xboobface@gmail.com>
Date: Sat, 11 Sep 2021 17:43:44 +0200
Subject: [PATCH] Make columns of RecordingsTable configurable

---
 .../table/SettingTableViewStateStore.java     | 109 +++++++++++++
 .../table/StatePersistingTableView.java       | 147 ++++++++++++++++++
 .../controls/table/TableViewStateStore.java   |  21 +++
 .../table/TableViewStateStoreException.java   |   9 ++
 .../java/ctbrec/ui/tabs/RecordingsTab.java    |  67 +-------
 common/src/main/java/ctbrec/Settings.java     |   9 +-
 6 files changed, 297 insertions(+), 65 deletions(-)
 create mode 100644 client/src/main/java/ctbrec/ui/controls/table/SettingTableViewStateStore.java
 create mode 100644 client/src/main/java/ctbrec/ui/controls/table/StatePersistingTableView.java
 create mode 100644 client/src/main/java/ctbrec/ui/controls/table/TableViewStateStore.java
 create mode 100644 client/src/main/java/ctbrec/ui/controls/table/TableViewStateStoreException.java

diff --git a/client/src/main/java/ctbrec/ui/controls/table/SettingTableViewStateStore.java b/client/src/main/java/ctbrec/ui/controls/table/SettingTableViewStateStore.java
new file mode 100644
index 00000000..1c59195c
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/table/SettingTableViewStateStore.java
@@ -0,0 +1,109 @@
+package ctbrec.ui.controls.table;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Map;
+
+import ctbrec.Config;
+import ctbrec.Settings;
+import javafx.scene.control.TableColumn.SortType;
+
+public class SettingTableViewStateStore implements TableViewStateStore {
+
+    private final Config config;
+    private final String settingPrefix;
+    private final String columnOrderSetting;
+    private final String columnWidthSetting;
+    private final String columnVisibilitySetting;
+    private final String sortColumnSetting;
+    private final String sortTypeSetting;
+
+    public SettingTableViewStateStore(Config config, String settingPrefix) {
+        this.config = config;
+        this.settingPrefix = settingPrefix;
+        columnOrderSetting = settingPrefix + "ColumnOrder";
+        columnWidthSetting = settingPrefix + "ColumnWidth";
+        columnVisibilitySetting = settingPrefix + "ColumnVisibility";
+        sortColumnSetting = settingPrefix + "SortColumn";
+        sortTypeSetting = settingPrefix + "SortType";
+    }
+
+    @Override
+    public List<String> loadColumnOrder() {
+        return loadSetting(columnOrderSetting);
+    }
+
+    @Override
+    public Map<String, Double> loadColumnWidths() {
+        return loadSetting(columnWidthSetting);
+    }
+
+    @Override
+    public Map<String, Boolean> loadColumnVisibility() {
+        return loadSetting(columnVisibilitySetting);
+    }
+
+    @Override
+    public String loadSortColumn() {
+        return loadSetting(sortColumnSetting);
+    }
+
+    @Override
+    public SortType loadSortType() {
+        return SortType.valueOf(loadSetting(sortTypeSetting));
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> T loadSetting(String name) {
+        try {
+            Field field = Settings.class.getDeclaredField(name);
+            return (T) field.get(config.getSettings());
+        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
+            throw new TableViewStateStoreException(e);
+        }
+    }
+
+    private void setSetting(String name, Object value) {
+        try {
+            Field field = Settings.class.getDeclaredField(name);
+            field.set(config.getSettings(), value); // NOSONAR
+        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
+            throw new TableViewStateStoreException(e);
+        }
+    }
+
+    @Override
+    public void saveColumnOrder(List<String> columnIds) throws IOException {
+        setSetting(columnOrderSetting, columnIds);
+        save();
+    }
+
+    @Override
+    public void saveColumnWidths(Map<String, Double> columnIdsToWidth) throws IOException {
+        setSetting(columnWidthSetting, columnIdsToWidth);
+        save();
+    }
+
+    @Override
+    public void saveColumnVisibility(Map<String, Boolean> columnIdsToVisibility) throws IOException {
+        setSetting(columnVisibilitySetting, columnIdsToVisibility);
+        save();
+    }
+
+    @Override
+    public void saveSorting(String columnId, SortType sortType) throws IOException {
+        setSetting(sortColumnSetting, columnId);
+        setSetting(sortTypeSetting, sortType.name());
+        save();
+    }
+
+    private void save() throws IOException {
+        config.save();
+    }
+
+    @Override
+    public String getName() {
+        return settingPrefix;
+    }
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/table/StatePersistingTableView.java b/client/src/main/java/ctbrec/ui/controls/table/StatePersistingTableView.java
new file mode 100644
index 00000000..86f2bfa6
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/table/StatePersistingTableView.java
@@ -0,0 +1,147 @@
+package ctbrec.ui.controls.table;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ctbrec.StringUtil;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+
+public class StatePersistingTableView<T> extends TableView<T> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(StatePersistingTableView.class);
+
+    private Instant initialized;
+    private TableViewStateStore stateStore;
+
+    public StatePersistingTableView(TableViewStateStore stateStore) {
+        super();
+        this.stateStore = stateStore;
+        setTableMenuButtonVisible(true);
+        initialized = Instant.now();
+    }
+
+    public void restoreState() {
+        restoreColumnOrder();
+        restoreColumnWidths();
+        restoreColumnVisibility();
+        restoreSorting();
+
+        addStateListeners();
+    }
+
+    private void addStateListeners() {
+        // column order
+        getColumns().addListener((ListChangeListener<? super TableColumn<T, ?>>) c -> saveColumnOrder());
+        // column visibility
+        getColumns().forEach(tc -> tc.visibleProperty().addListener((obs, oldV, newV) -> saveColumnVisibility()));
+        // column width
+        getColumns().forEach(tc -> tc.widthProperty().addListener((obs, oldV, newV) -> saveColumnWidths()));
+        // sort order
+        getSortOrder().addListener((ListChangeListener<? super TableColumn<T, ?>>) c -> saveSorting());
+        getColumns().forEach(tc -> tc.sortTypeProperty().addListener((obs, oldV, newV) -> saveSorting()));
+    }
+
+    protected void restoreColumnVisibility() {
+        Map<String, Boolean> visibility = stateStore.loadColumnVisibility();
+        for (TableColumn<T, ?> tc : getColumns()) {
+            tc.setVisible(visibility.getOrDefault(tc.getId(), true));
+        }
+    }
+
+    protected void restoreColumnWidths() {
+        Map<String, Double> widths = stateStore.loadColumnWidths();
+        for (TableColumn<T, ?> tc : getColumns()) {
+            tc.setPrefWidth(widths.getOrDefault(tc.getId(), tc.getWidth()));
+        }
+    }
+
+    protected void restoreColumnOrder() {
+        List<String> order = stateStore.loadColumnOrder();
+        ObservableList<TableColumn<T, ?>> tableColumns = getColumns();
+        for (var i = 0; i < order.size(); i++) {
+            for (var j = 0; j < getColumns().size(); j++) {
+                if (Objects.equals(order.get(i), tableColumns.get(j).getId())) {
+                    TableColumn<T, ?> col = tableColumns.get(j);
+                    tableColumns.remove(j);
+                    tableColumns.add(Math.min(i, tableColumns.size()), col);
+                }
+            }
+        }
+    }
+
+    protected void restoreSorting() {
+        String sortCol = stateStore.loadSortColumn();
+        if (StringUtil.isNotBlank(sortCol)) {
+            for (TableColumn<T, ?> col : getColumns()) {
+                if (Objects.equals(sortCol, col.getId())) {
+                    col.setSortType(stateStore.loadSortType());
+                    getSortOrder().clear();
+                    getSortOrder().add(col);
+                    break;
+                }
+            }
+        }
+    }
+
+    public void saveState() {
+        saveColumnOrder();
+        saveColumnWidths();
+        saveColumnVisibility();
+        saveSorting();
+    }
+
+    protected void saveSorting() {
+        if (!getSortOrder().isEmpty()) {
+            TableColumn<T, ?> col = getSortOrder().get(0);
+            saveSetting(() -> stateStore.saveSorting(col.getId(), col.getSortType()));
+        } else {
+            saveSetting(() -> stateStore.saveSorting(null, stateStore.loadSortType()));
+        }
+    }
+
+    protected void saveColumnVisibility() {
+        saveSetting(() -> {
+            Map<String, Boolean> columnIdToVisible = getColumns().stream().collect(Collectors.toMap(TableColumn::getId, TableColumn::isVisible));
+            stateStore.saveColumnVisibility(columnIdToVisible);
+        });
+    }
+
+    protected void saveColumnWidths() {
+        saveSetting(() -> {
+            Map<String, Double> columnIdToWidth = getColumns().stream().collect(Collectors.toMap(TableColumn::getId, TableColumn::getWidth));
+            stateStore.saveColumnWidths(columnIdToWidth);
+        });
+    }
+
+    protected void saveColumnOrder() {
+        saveSetting(() -> {
+            List<String> tableIds = getColumns().stream().map(TableColumn::getId).collect(Collectors.toList());
+            stateStore.saveColumnOrder(tableIds);
+        });
+    }
+
+    private void saveSetting(ThrowingRunnable r) {
+        if (Duration.between(initialized, Instant.now()).getSeconds() > 1) {
+            try {
+                r.run();
+            } catch (Exception e) {
+                LOG.error("Couldn't safe table view state with prefix {}", stateStore.getName(), e);
+            }
+        }
+    }
+
+    @FunctionalInterface
+    private interface ThrowingRunnable {
+        void run() throws Exception; // NOSONAR
+    }
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/table/TableViewStateStore.java b/client/src/main/java/ctbrec/ui/controls/table/TableViewStateStore.java
new file mode 100644
index 00000000..44a9b8e5
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/table/TableViewStateStore.java
@@ -0,0 +1,21 @@
+package ctbrec.ui.controls.table;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import javafx.scene.control.TableColumn.SortType;
+
+public interface TableViewStateStore {
+    List<String> loadColumnOrder();
+    Map<String, Double> loadColumnWidths();
+    Map<String, Boolean> loadColumnVisibility();
+    String loadSortColumn();
+    SortType loadSortType();
+
+    void saveColumnOrder(List<String> columnIds) throws IOException;
+    void saveColumnWidths(Map<String, Double> columnIdsToWidth) throws IOException;
+    void saveColumnVisibility(Map<String, Boolean> columnIdsToVisibility) throws IOException;
+    void saveSorting(String columnId, SortType sortType) throws IOException;
+    String getName();
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/table/TableViewStateStoreException.java b/client/src/main/java/ctbrec/ui/controls/table/TableViewStateStoreException.java
new file mode 100644
index 00000000..52504e30
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/table/TableViewStateStoreException.java
@@ -0,0 +1,9 @@
+package ctbrec.ui.controls.table;
+
+public class TableViewStateStoreException extends RuntimeException {
+
+    public TableViewStateStoreException(Exception e) {
+        super(e);
+    }
+
+}
diff --git a/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java b/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java
index 57b0dee9..9bbac023 100644
--- a/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java
+++ b/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java
@@ -55,6 +55,8 @@ import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
 import ctbrec.ui.controls.DateTimeCellFactory;
 import ctbrec.ui.controls.Dialogs;
 import ctbrec.ui.controls.Toast;
+import ctbrec.ui.controls.table.SettingTableViewStateStore;
+import ctbrec.ui.controls.table.StatePersistingTableView;
 import ctbrec.ui.menu.ModelMenuContributor;
 import ctbrec.ui.tabs.recorded.ModelName;
 import javafx.application.Platform;
@@ -80,8 +82,6 @@ import javafx.scene.control.SelectionMode;
 import javafx.scene.control.Tab;
 import javafx.scene.control.TableCell;
 import javafx.scene.control.TableColumn;
-import javafx.scene.control.TableColumn.SortType;
-import javafx.scene.control.TableView;
 import javafx.scene.control.Tooltip;
 import javafx.scene.input.ContextMenuEvent;
 import javafx.scene.input.KeyCode;
@@ -109,7 +109,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
 
     FlowPane grid = new FlowPane();
     ScrollPane scrollPane = new ScrollPane();
-    TableView<JavaFxRecording> table = new TableView<>();
+    SettingTableViewStateStore tableStateStore = new SettingTableViewStateStore(Config.getInstance(), "recordingsTable");
+    StatePersistingTableView<JavaFxRecording> table = new StatePersistingTableView<>(tableStateStore);
     ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
     ContextMenu popup;
     ProgressBar spaceLeft;
@@ -216,7 +217,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
         root.setCenter(scrollPane);
         setContent(root);
 
-        restoreState();
+        table.restoreState();
     }
 
     public boolean isDownloadRunning() {
@@ -855,62 +856,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
 
     @Override
     public void onShutdown() {
-        if (!table.getSortOrder().isEmpty()) {
-            TableColumn<JavaFxRecording, ?> col = table.getSortOrder().get(0);
-            Config.getInstance().getSettings().recordingsSortColumn = col.getText();
-            Config.getInstance().getSettings().recordingsSortType = col.getSortType().toString();
-        }
-        int columns = table.getColumns().size();
-        var columnWidths = new double[columns];
-        var columnIds = new String[columns];
-        for (var i = 0; i < columnWidths.length; i++) {
-            columnWidths[i] = table.getColumns().get(i).getWidth();
-            columnIds[i] = table.getColumns().get(i).getId();
-        }
-        Config.getInstance().getSettings().recordingsColumnWidths = columnWidths;
-        Config.getInstance().getSettings().recordingsColumnIds = columnIds;
-    }
-
-    private void restoreState() {
-        restoreColumnOrder();
-        restoreColumnWidths();
-        restoreSorting();
-    }
-
-    private void restoreSorting() {
-        String sortCol = Config.getInstance().getSettings().recordingsSortColumn;
-        if (StringUtil.isNotBlank(sortCol)) {
-            for (TableColumn<JavaFxRecording, ?> col : table.getColumns()) {
-                if (Objects.equals(sortCol, col.getText())) {
-                    col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordingsSortType));
-                    table.getSortOrder().clear();
-                    table.getSortOrder().add(col);
-                    break;
-                }
-            }
-        }
-    }
-
-    private void restoreColumnOrder() {
-        String[] columnIds = Config.getInstance().getSettings().recordingsColumnIds;
-        ObservableList<TableColumn<JavaFxRecording, ?>> columns = table.getColumns();
-        for (var i = 0; i < columnIds.length; i++) {
-            for (var j = 0; j < table.getColumns().size(); j++) {
-                if (Objects.equals(columnIds[i], columns.get(j).getId())) {
-                    TableColumn<JavaFxRecording, ?> col = columns.get(j);
-                    columns.remove(j); // NOSONAR
-                    columns.add(i, col);
-                }
-            }
-        }
-    }
-
-    private void restoreColumnWidths() {
-        double[] columnWidths = Config.getInstance().getSettings().recordingsColumnWidths;
-        if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
-            for (var i = 0; i < columnWidths.length; i++) {
-                table.getColumns().get(i).setPrefWidth(columnWidths[i]);
-            }
-        }
+        table.saveState();
     }
 }
diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java
index 38bb0cf7..337b09fd 100644
--- a/common/src/main/java/ctbrec/Settings.java
+++ b/common/src/main/java/ctbrec/Settings.java
@@ -148,12 +148,13 @@ public class Settings {
     public List<String> recordedModelsDisabledTableColumns = new ArrayList<>();
     public String recordedModelsSortColumn = "";
     public String recordedModelsSortType = "";
-    public double[] recordingsColumnWidths = new double[0];
-    public String[] recordingsColumnIds = new String[0];
+    public List<String> recordingsTableColumnOrder = new ArrayList<>();
+    public Map<String, Boolean> recordingsTableColumnVisibility = new HashMap<>();
+    public Map<String, Double> recordingsTableColumnWidth = new HashMap<>();
+    public String recordingsTableSortColumn = "";
+    public String recordingsTableSortType = "";
     public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
     public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
-    public String recordingsSortColumn = "";
-    public String recordingsSortType = "";
     public List<Model> recordLater = new ArrayList<>();
     public boolean recordSingleFile = false;
     public boolean removeRecordingAfterPostProcessing = false;