/** @return A list of the settable fields. */ public static List<Field> getSettableFields() { // Init the search in the root package. Reflections reflections = new Reflections("org.saucistophe", new FieldAnnotationsScanner()); Set<Field> annotatedFields = reflections.getFieldsAnnotatedWith(SettingsField.class); // Turn the set to a list to sort it. List<Field> fieldsList = new ArrayList<>(annotatedFields); fieldsList.sort( (Field field1, Field field2) -> { // Retrieve the fields info. SettingsField fieldInfo1 = field1.getAnnotation(SettingsField.class); SettingsField fieldInfo2 = field2.getAnnotation(SettingsField.class); // If the name wasn't set, get the field's declared name. String actualName1 = fieldInfo1.name().isEmpty() ? field1.getName() : fieldInfo1.name(); String actualName2 = fieldInfo2.name().isEmpty() ? field2.getName() : fieldInfo2.name(); // Elaborate a sortable string representation. String sortableString1 = fieldInfo1.category() + "." + actualName1; String sortableString2 = fieldInfo2.category() + "." + actualName2; return sortableString1.compareTo(sortableString2); }); return fieldsList; }
/** * Reads the settings from the settings JSON file, and call all callbacks. Typically used during * the application init. */ public static void readFromFile() { Map<String, Map<String, Object>> categories; Gson gson = new Gson(); // Read from file try (BufferedReader bufferedReader = new BufferedReader(new FileReader("settings.json"))) { @SuppressWarnings("unchecked") Map<String, Map<String, Object>> dummyCategories = gson.fromJson(bufferedReader, Map.class); categories = dummyCategories; // Then, for each setting, try to find its value. for (Field settableField : getSettableFields()) { SettingsField fieldInfo = settableField.getAnnotation(SettingsField.class); if (categories.containsKey(fieldInfo.category())) { Map<String, Object> category = categories.get(fieldInfo.category()); // If the name wasn't set, get the field's declared n0ame. String actualName = fieldInfo.name().isEmpty() ? settableField.getName() : fieldInfo.name(); if (category.containsKey(actualName)) { // Deserialize from String. set( settableField, toObject(settableField.getType(), (String) category.get(actualName))); } } } } catch (JsonSyntaxException exception) { // On incorrect properties, log a warning. Logger.getLogger(SettingsHandler.class.getName()) .log(Level.WARNING, "Syntax error in JSON settings file."); Logger.getLogger(SettingsHandler.class.getName()).log(Level.WARNING, null, exception); } catch (IOException exception) { // If the file is not readable, or does not exist. Logger.getLogger(SettingsHandler.class.getName()) .log(Level.WARNING, "Could not read JSON settings file."); } finally { triggerCallbacks(); } }
/** Saves the state of the current settings to a file. */ public static void saveToFile() { // Create a map of fields, indexed on categories then names. // Those are sorted maps, to preserve alphabetical order. SortedMap<String, SortedMap<String, Object>> categories = new TreeMap<>(); for (Field field : getSettableFields()) { // Retrieve the field info. SettingsField fieldInfo = field.getAnnotation(SettingsField.class); // Create a category map, if it doesn't exist. SortedMap<String, Object> category; if (categories.containsKey(fieldInfo.category())) { category = categories.get(fieldInfo.category()); } else { category = new TreeMap<>(); categories.put(fieldInfo.category(), category); } // If the name wasn't set, get the field's declared name. String actualName = fieldInfo.name().isEmpty() ? field.getName() : fieldInfo.name(); // Put the field and its value, as a string. category.put(actualName, get(field).toString()); } // Export the result to a json file. GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setPrettyPrinting(); Gson gson = gsonBuilder.create(); String jsonString = gson.toJson(categories); // Write to file. try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("settings.json"))) { bufferedWriter.write(jsonString); } catch (IOException ex) { // If the file can't be written, it's an error. Logger.getLogger(SettingsHandler.class.getName()).log(Level.SEVERE, null, ex); } }
/** Shows a swing dialog to eit the available settings. */ public static void showSettingsDialog() { // Create a tabbed panel, one tab per category. JTabbedPane categoriesPane = new JTabbedPane(); // A list of actions taken when validating, i.e. when setting all settings to the input values. List<Supplier<Boolean>> actions = new ArrayList<>(); // Create a panel. // For each available setting: for (Field settingField : getSettableFields()) { // Retrieve the annotation. SettingsField fieldInfo = settingField.getAnnotation(SettingsField.class); // Search for an existing category panel. Integer categoryIndex = null; for (int candidateCategoryIndex = 0; candidateCategoryIndex < categoriesPane.getTabCount(); candidateCategoryIndex++) { if (categoriesPane.getTitleAt(candidateCategoryIndex).equals(fieldInfo.category())) { categoryIndex = candidateCategoryIndex; break; } } // If the category exists, retrieve its pane, otherwise create it. JPanel categoryPanel = null; if (categoryIndex != null) { categoryPanel = (JPanel) categoriesPane.getComponentAt(categoryIndex); } else { categoryPanel = new JPanel(); categoryPanel.setLayout(new BoxLayout(categoryPanel, BoxLayout.Y_AXIS)); categoriesPane.add(categoryPanel, fieldInfo.category()); } // If the name wasn't set, get the field's declared name. String actualName = fieldInfo.name().isEmpty() ? settingField.getName() : fieldInfo.name(); // Prepare a label text. String labelText = fieldInfo.description().isEmpty() ? actualName : fieldInfo.description(); // Create an optional label for component that need it. JLabel label = new JLabel(labelText); JComponent editionComponent; // Handle enums. if (settingField.getType().isEnum()) { JComboBox<?> comboBox = new JComboBox<>(settingField.getType().getEnumConstants()); comboBox.setSelectedItem(get(settingField)); actions.add( () -> { set(settingField, comboBox.getSelectedItem()); return true; }); // Add a label then the component. editionComponent = new JPanel(); editionComponent.add(label); editionComponent.add(comboBox); } else { // Handle primitive types switch (settingField.getType().getSimpleName().toLowerCase()) { case "boolean": JCheckBox checkbox = new JCheckBox(labelText, (boolean) get(settingField)); editionComponent = checkbox; actions.add( () -> { set(settingField, checkbox.isSelected()); return true; }); break; case "string": // If there are possible values, use a combobox. if (fieldInfo.possibleValues().length != 0) { JComboBox<String> comboBox = new JComboBox<>(fieldInfo.possibleValues()); actions.add( () -> { set(settingField, comboBox.getSelectedItem()); return true; }); // Add a label then the component. editionComponent = new JPanel(); editionComponent.add(label); editionComponent.add(comboBox); } else { // Otherwise, use a simple text field. JTextField textField = new JTextField((String) get(settingField)); actions.add( () -> { set(settingField, textField.getText()); return true; }); // Add a label then the component. editionComponent = new JPanel(); editionComponent.add(label); editionComponent.add(textField); } break; case "int": case "integer": int currentIntValue = (int) get(settingField); SpinnerModel spinnerModel = new SpinnerNumberModel( currentIntValue, fieldInfo.minValue(), fieldInfo.maxValue(), 1); JSpinner spinner = new JSpinner(spinnerModel); spinner.setValue(currentIntValue); actions.add( () -> { set(settingField, spinner.getValue()); return true; }); // Add a label then the component. editionComponent = new JPanel(); editionComponent.add(label); editionComponent.add(spinner); break; default: editionComponent = new JTextField("Unknown setting type"); editionComponent.setEnabled(false); break; } } categoryPanel.add(editionComponent); // Add a fancy tooltip. if (fieldInfo.description() != null && !fieldInfo.description().isEmpty()) { editionComponent.setToolTipText(fieldInfo.description()); } } // Put the panel in a modal frame. JDialog dialog = new JDialog((Frame) null, "Settings", true); dialog.add(categoriesPane); // Add a validation and cancel button. JPanel bottomButtons = new JPanel(new FlowLayout(TRAILING)); dialog.add(bottomButtons, BorderLayout.SOUTH); JButton okButton = new JButton("OK"); { okButton.addActionListener( e -> { for (Supplier<Boolean> action : actions) { action.get(); } saveToFile(); // Call all callbacks on all categories. Set<Method> callbacks = getSettingsCallbacks(); callbacks .stream() .forEach( m -> { try { m.invoke(null); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { Logger.getLogger(SettingsHandler.class.getName()) .log(Level.SEVERE, null, ex); } }); dialog.dispose(); }); } bottomButtons.add(okButton); JButton cancelButton = new JButton("Cancel"); { cancelButton.addActionListener( e -> { dialog.dispose(); }); } bottomButtons.add(cancelButton); dialog.pack(); dialog.setVisible(true); }