package ctbrec.ui.settings; import java.io.IOException; import java.time.LocalTime; import java.util.List; import java.util.Objects; import java.util.Optional; import ctbrec.Config; import ctbrec.Settings; import ctbrec.StringUtil; import ctbrec.ui.controls.DirectorySelectionBox; import ctbrec.ui.controls.ProgramSelectionBox; import ctbrec.ui.controls.TimePicker; import ctbrec.ui.controls.range.DiscreteRange; import ctbrec.ui.controls.range.RangeSlider; import ctbrec.ui.settings.api.ExclusiveSelectionProperty; import ctbrec.ui.settings.api.LocalTimeProperty; import ctbrec.ui.settings.api.Preferences; import ctbrec.ui.settings.api.PreferencesStorage; import ctbrec.ui.settings.api.Setting; import ctbrec.ui.settings.api.SimpleDirectoryProperty; import ctbrec.ui.settings.api.SimpleFileProperty; import ctbrec.ui.settings.api.SimpleRangeProperty; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ListProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.Property; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.HBox; import javafx.util.converter.NumberStringConverter; import lombok.extern.slf4j.Slf4j; @Slf4j public class CtbrecPreferencesStorage implements PreferencesStorage { public static final String PATTERN_NOT_A_DIGIT = "[^\\d]"; public static final String COULDNT_SAVE_MSG = "Couldn't save config setting"; private Config config; private Settings settings; private Preferences prefs; public CtbrecPreferencesStorage(Config config) { this.config = config; this.settings = config.getSettings(); } public void setPreferences(Preferences prefs) { this.prefs = prefs; } @Override public void save(Preferences preferences) throws IOException { throw new RuntimeException("not implemented"); } @Override public void load(Preferences preferences) { throw new RuntimeException("not implemented"); } @Override public Node createGui(Setting setting) throws NoSuchFieldException, IllegalAccessException { config.disableSaving(); try { Property prop = setting.getProperty(); if (prop instanceof ExclusiveSelectionProperty) { return createRadioGroup(setting); } else if (prop instanceof SimpleRangeProperty) { return createRangeSlider(setting); } else if (prop instanceof SimpleDirectoryProperty) { return createDirectorySelector(setting); } else if (prop instanceof SimpleFileProperty) { return createFileSelector(setting); } else if (prop instanceof LocalTimeProperty) { return createTimeSelector(setting); } else if (prop instanceof IntegerProperty) { return createIntegerProperty(setting); } else if (prop instanceof LongProperty) { return createLongProperty(setting); } else if (prop instanceof BooleanProperty) { return createBooleanProperty(setting); } else if (prop instanceof ListProperty) { return createComboBox(setting); } else if (prop instanceof StringProperty) { return createStringProperty(setting); } else { return new Label("Unsupported Type for key " + setting.getKey() + ": " + setting.getProperty()); } } finally { config.enableSaving(); } } private Node createRadioGroup(Setting setting) { ExclusiveSelectionProperty prop = (ExclusiveSelectionProperty) setting.getProperty(); var toggleGroup = new ToggleGroup(); var optionA = new RadioButton(prop.getOptionA()); optionA.setSelected(prop.getValue()); optionA.setToggleGroup(toggleGroup); var optionB = new RadioButton(prop.getOptionB()); optionB.setSelected(!optionA.isSelected()); optionB.setToggleGroup(toggleGroup); optionA.selectedProperty().bindBidirectional(prop); prop.addListener((obs, oldV, newV) -> saveValue(() -> { var field = Settings.class.getField(setting.getKey()); field.set(settings, newV); // NOSONAR if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) { runRestartRequiredCallback(); } config.save(); })); var row = new HBox(); row.getChildren().addAll(optionA, optionB); HBox.setMargin(optionA, new Insets(5)); HBox.setMargin(optionB, new Insets(5)); return row; } private void runRestartRequiredCallback() { Optional.ofNullable(prefs).map(Preferences::getRestartRequiredCallback).ifPresent(r -> { try { r.run(); } catch (RuntimeException e) { log.warn("Error while calling \"restart required\" callback", e); } }); } @SuppressWarnings("unchecked") private Node createRangeSlider(Setting setting) { SimpleRangeProperty rangeProperty = (SimpleRangeProperty) setting.getProperty(); DiscreteRange range = (DiscreteRange) rangeProperty.getRange(); List labels = (List) range.getLabels(); List values = range.getTicks(); RangeSlider resolutionRange = new RangeSlider<>(rangeProperty.getRange()); resolutionRange.setShowTickMarks(true); resolutionRange.setShowTickLabels(true); int lowValue = getRangeSliderValue(values, labels, Config.getInstance().getSettings().minimumResolution); resolutionRange.setLow(lowValue >= 0 ? lowValue : values.get(0)); int highValue = getRangeSliderValue(values, labels, Config.getInstance().getSettings().maximumResolution); resolutionRange.setHigh(highValue >= 0 ? highValue : values.get(values.size() - 1)); resolutionRange.getLow().addListener((obs, o, n) -> saveValue(() -> { int newV = labels.get(n.intValue()); var field = Settings.class.getField(rangeProperty.getLowKey()); field.set(settings, newV); // NOSONAR config.save(); })); resolutionRange.getHigh().addListener((obs, o, n) -> saveValue(() -> { int newV = labels.get(n.intValue()); var field = Settings.class.getField(rangeProperty.getHighKey()); field.set(settings, newV); // NOSONAR config.save(); })); return resolutionRange; } private int getRangeSliderValue(List values, List labels, int value) { for (var i = 0; i < labels.size(); i++) { var label = labels.get(i).intValue(); if (label == value) { return values.get(i); } } return -1; } private Node createFileSelector(Setting setting) { var programSelector = new ProgramSelectionBox(""); programSelector.fileProperty().addListener((obs, o, n) -> saveValue(() -> { String path = n; var field = Settings.class.getField(setting.getKey()); String oldValue = (String) field.get(settings); if (!Objects.equals(path, oldValue)) { field.set(settings, path); // NOSONAR if (setting.doesNeedRestart()) { runRestartRequiredCallback(); } config.save(); } })); StringProperty property = (StringProperty) setting.getProperty(); programSelector.fileProperty().bindBidirectional(property); return programSelector; } private Node createDirectorySelector(Setting setting) { var directorySelector = new DirectorySelectionBox(""); directorySelector.prefWidth(400); directorySelector.fileProperty().addListener((obs, o, n) -> saveValue(() -> { String path = n; var field = Settings.class.getField(setting.getKey()); String oldValue = (String) field.get(settings); if (!Objects.equals(path, oldValue)) { field.set(settings, path); // NOSONAR if (setting.doesNeedRestart()) { runRestartRequiredCallback(); } config.save(); } })); StringProperty property = (StringProperty) setting.getProperty(); directorySelector.fileProperty().bindBidirectional(property); return directorySelector; } private Node createTimeSelector(Setting setting) { LocalTime time = (LocalTime) setting.getProperty().getValue(); var timePicker = new TimePicker(time); timePicker.valueProperty().addListener((obs, o, n) -> saveValue(() -> { var field = Settings.class.getField(setting.getKey()); LocalTime oldValue = (LocalTime) field.get(settings); if (!Objects.equals(n, oldValue)) { field.set(settings, n); // NOSONAR if (setting.doesNeedRestart()) { runRestartRequiredCallback(); } config.save(); } })); return timePicker; } private Node createStringProperty(Setting setting) { var ctrl = new TextField(); ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { var field = Settings.class.getField(setting.getKey()); field.set(settings, newV); // NOSONAR if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) { runRestartRequiredCallback(); } config.save(); })); StringProperty prop = (StringProperty) setting.getProperty(); ctrl.textProperty().bindBidirectional(prop); return ctrl; } @SuppressWarnings("unchecked") private Node createIntegerProperty(Setting setting) { var ctrl = new TextField(); ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { if (!newV.matches("\\d*")) { ctrl.setText(newV.replaceAll(PATTERN_NOT_A_DIGIT, "")); } if (!ctrl.getText().isEmpty()) { var field = Settings.class.getField(setting.getKey()); field.set(settings, Integer.parseInt(ctrl.getText())); // NOSONAR if (setting.doesNeedRestart() && !Objects.equals(oldV, newV) && prefs != null) { runRestartRequiredCallback(); } config.save(); } })); Property prop = setting.getProperty(); ctrl.textProperty().bindBidirectional(prop, new NumberStringConverter()); return ctrl; } @SuppressWarnings("unchecked") private Node createLongProperty(Setting setting) { var ctrl = new TextField(); ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { if (!newV.matches("\\d*")) { ctrl.setText(newV.replaceAll(PATTERN_NOT_A_DIGIT, "")); } if (!ctrl.getText().isEmpty()) { var value = Long.parseLong(ctrl.getText()); if (setting.getConverter() != null) { value = (long) setting.getConverter().convertFrom(value); } var field = Settings.class.getField(setting.getKey()); field.set(settings, value); // NOSONAR if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) { runRestartRequiredCallback(); } config.save(); } })); Property prop = setting.getProperty(); ctrl.textProperty().bindBidirectional(prop, new NumberStringConverter()); return ctrl; } private Node createBooleanProperty(Setting setting) { var ctrl = new CheckBox(); ctrl.selectedProperty().addListener((obs, oldV, newV) -> saveValue(() -> { var field = Settings.class.getField(setting.getKey()); field.set(settings, newV); // NOSONAR if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) { runRestartRequiredCallback(); } config.save(); })); BooleanProperty prop = (BooleanProperty) setting.getProperty(); ctrl.selectedProperty().bindBidirectional(prop); return ctrl; } @SuppressWarnings({ "rawtypes", "unchecked" }) private Node createComboBox(Setting setting) throws NoSuchFieldException, IllegalAccessException { ListProperty listProp = (ListProperty) setting.getProperty(); ComboBox comboBox = new ComboBox(listProp); var field = Settings.class.getField(setting.getKey()); Object value = field.get(settings); if (StringUtil.isNotBlank(value.toString())) { if (setting.getConverter() != null) { comboBox.getSelectionModel().select(setting.getConverter().convertTo(value)); } else { comboBox.getSelectionModel().select(value); } } comboBox.valueProperty().addListener((obs, oldV, newV) -> saveValue(() -> { log.debug("Saving setting {}", setting.getKey()); if (setting.getConverter() != null) { field.set(settings, setting.getConverter().convertFrom(newV)); // NOSONAR } else { field.set(settings, newV); // NOSONAR } if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) { runRestartRequiredCallback(); } config.save(); })); if (setting.getChangeListener() != null) { comboBox.valueProperty().addListener((ChangeListener) setting.getChangeListener()); } return comboBox; } private void saveValue(Exec exe) { try { exe.run(); } catch (Exception e) { log.error(COULDNT_SAVE_MSG, e); } } @FunctionalInterface private interface Exec { public void run() throws IllegalAccessException, IOException, NoSuchFieldException; } }