package ctbrec.ui.settings; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.time.LocalTime; // import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.SimpleJoinedStringListProperty; import ctbrec.ui.settings.api.SimpleRangeProperty; import ctbrec.io.BoundField; 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.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.HBox; import javafx.util.converter.NumberStringConverter; public class CtbrecPreferencesStorage implements PreferencesStorage { private static final Logger LOG = LoggerFactory.getLogger(CtbrecPreferencesStorage.class); 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 SimpleJoinedStringListProperty) { return createStringListProperty(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(() -> { if (setIfChanged(setting.getKey(), newV)) { if (setting.doesNeedRestart()) { 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()); if (setIfChanged(rangeProperty.getLowKey(), newV)) { config.save(); } })); resolutionRange.getHigh().addListener((obs, o, n) -> saveValue(() -> { int newV = labels.get(n.intValue()); if (setIfChanged(rangeProperty.getHighKey(), newV)) { 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(() -> { if (setIfChanged(setting.getKey(), n)) { 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(() -> { if (setIfChanged(setting.getKey(), n)) { 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(() -> { if (setIfChanged(setting.getKey(), n)) { if (setting.doesNeedRestart()) { runRestartRequiredCallback(); } config.save(); } })); return timePicker; } private Node createStringProperty(Setting setting) { var ctrl = new TextField(); ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { if (setIfChanged(setting.getKey(), newV)) { if (setting.doesNeedRestart()) { runRestartRequiredCallback(); } config.save(); } })); StringProperty prop = (StringProperty) setting.getProperty(); ctrl.textProperty().bindBidirectional(prop); return ctrl; } private Node createStringListProperty(Setting setting) { var ctrl = new TextArea(); StringProperty prop = (StringProperty) setting.getProperty(); ctrl.textProperty().bindBidirectional(prop); prop.addListener((obs, oldV, newV) -> saveValue(() -> { //setUnchecked(setting.getKey(), Arrays.asList(newV.split("\n"))); if (setting.doesNeedRestart()) { runRestartRequiredCallback(); } config.save(); })); 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() && setIfChanged(setting.getKey(), Integer.parseInt(ctrl.getText()))) { if (setting.doesNeedRestart() && 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); } if (setIfChanged(setting.getKey(), value)) { 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(() -> { if (setIfChanged(setting.getKey(), newV)) { if (setting.doesNeedRestart()) { runRestartRequiredCallback(); } config.save(); } })); BooleanProperty prop = (BooleanProperty) setting.getProperty(); ctrl.selectedProperty().bindBidirectional(prop); return ctrl; } @SuppressWarnings({ "rawtypes", "unchecked" }) private Node createComboBox(Setting setting) throws IllegalAccessException, NoSuchFieldException { ListProperty listProp = (ListProperty) setting.getProperty(); ComboBox comboBox = new ComboBox(listProp); Object value = BoundField.of(settings, setting.getKey()).get(); 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 (setIfChanged(setting.getKey(), setting.getConverter() != null ? setting.getConverter().convertFrom(newV) : newV)) { if (setting.doesNeedRestart()) { runRestartRequiredCallback(); } config.save(); } })); if (setting.getChangeListener() != null) { comboBox.valueProperty().addListener((ChangeListener) setting.getChangeListener()); } return comboBox; } private boolean setIfChanged(String key, Object n) throws IllegalAccessException, NoSuchFieldException, InvocationTargetException { var field = BoundField.of(settings, key); var o = field.get(); if (!Objects.equals(n, o)) { if (n instanceof List && o instanceof List) { var list = (List)o; list.clear(); list.addAll((List)n); } else { field.set(n); // NOSONAR } return true; } return false; } // private boolean setUnchecked(String key, Object n) throws IllegalAccessException, NoSuchFieldException, InvocationTargetException { // var field = BoundField.of(settings, key); // var o = field.get(); // if (n instanceof List && o instanceof List) { // var list = (List)o; // list.clear(); // list.addAll((List)n); // } else { // field.set(n); // NOSONAR // } // return true; // } 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, NoSuchMethodException, InvocationTargetException; } }