package ctbrec.ui.settings.api; import static java.util.Optional.*; import java.io.IOException; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.ui.controls.SearchBox; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; public class Preferences { private static final Logger LOG = LoggerFactory.getLogger(Preferences.class); private Category[] categories; private TreeView categoryTree; private PreferencesStorage preferencesStorage; private Runnable restartRequiredCallback = () -> {}; private Preferences(PreferencesStorage preferencesStorage, Category...categories) { this.preferencesStorage = preferencesStorage; this.categories = categories; for (Category category : categories) { assignPreferencesStorage(category, preferencesStorage); } } private void assignPreferencesStorage(Category cat, PreferencesStorage preferencesStorage) { if(cat.hasSubCategories()) { for (Category sub : cat.getSubCategories()) { assignPreferencesStorage(sub, preferencesStorage); } } else { for (Group group : cat.getGroups()) { for (Setting setting : group.getSettings()) { setting.setPreferencesStorage(preferencesStorage); } } } } public static Preferences of(PreferencesStorage preferencesStorage, Category... categories) { return new Preferences(preferencesStorage, categories); } public void save() throws IOException { preferencesStorage.save(this); } Category[] getCategories() { return categories; } public Region getView(boolean withNavigation) { var search = new SearchBox(true); search.textProperty().addListener(this::filterTree); TreeItem categoryTreeItems = createCategoryTree(categories, new TreeItem<>(), null); categoryTree = new TreeView<>(categoryTreeItems); categoryTree.showRootProperty().set(false); var leftSide = new VBox(search, categoryTree); VBox.setVgrow(categoryTree, Priority.ALWAYS); VBox.setMargin(search, new Insets(2)); VBox.setMargin(categoryTree, new Insets(2)); var main = new BorderPane(); if (withNavigation) { main.setLeft(leftSide); } main.setCenter(new Label("Center")); BorderPane.setMargin(leftSide, new Insets(2)); categoryTree.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> { if (newV != null) { Category cat = newV.getValue(); Node gui = cat.getGuiOrElse(() -> createGui(cat)); BorderPane.setMargin(gui, new Insets(10)); main.setCenter(gui); } }); categoryTree.getSelectionModel().select(0); return main; } public Region getView() { return getView(true); } private void filterTree(ObservableValue obs, String oldV, String newV) { String q = ofNullable(newV).orElse("").toLowerCase().trim(); TreeItem filteredCategoryTree = createCategoryTree(categories, new TreeItem<>(), q); categoryTree.setRoot(filteredCategoryTree); expandAll(categoryTree.getRoot()); TreeItem parent = categoryTree.getRoot(); while (!parent.getChildren().isEmpty()) { parent = parent.getChildren().get(0); categoryTree.getSelectionModel().select(parent); } for (Category category : categories) { if (q.length() > 2) { HighlightingSupport.highlightMatches(category, q); } else { HighlightingSupport.removeHighlights(category); } } } private void expandAll(TreeItem treeItem) { treeItem.setExpanded(true); for (TreeItem child : treeItem.getChildren()) { expandAll(child); } } private Node createGui(Category cat) { try { if (cat.hasSubCategories()) { return new Label(cat.getName()); } else if(cat.hasGroups()) { return createPaneWithGroups(cat); } else { return createGrid(cat.getGroups()[0].getSettings()); } } catch(Exception e) { LOG.error("Error creating the GUI", e); return new Label(e.getLocalizedMessage()); } } private Node createPaneWithGroups(Category cat) throws Exception { var pane = new VBox(); for (Group grp : cat.getGroups()) { var groupLabel = new Label(grp.getName()); groupLabel.getStyleClass().add("settings-group-label"); VBox.setMargin(groupLabel, new Insets(20, 0, 10, 20)); pane.getChildren().add(groupLabel); Node parameterGrid = createGrid(grp.getSettings()); pane.getChildren().add(parameterGrid); VBox.setMargin(parameterGrid, new Insets(0, 0, 0, 40)); } return pane; } private Node createGrid(Setting[] settings) throws Exception { var pane = new GridPane(); pane.setHgap(2); pane.vgapProperty().bind(pane.hgapProperty()); var row = 0; for (Setting setting : settings) { var node = setting.getGui(); var label = new Label(setting.getName()); label.setMinHeight(34); label.labelForProperty().set(node); label.setTooltip(new Tooltip(setting.getName())); pane.addRow(row++, label, node); GridPane.setVgrow(label, Priority.ALWAYS); GridPane.setValignment(label, VPos.CENTER); GridPane.setMargin(node, new Insets(5, 0, 5, 10)); GridPane.setValignment(node, VPos.CENTER); GridPane.setHgrow(node, Priority.ALWAYS); } return pane; } /** * Creates a tree of the given categories. Filters out categories, which don't match the filter * @param filter may be null */ private TreeItem createCategoryTree(Category[] categories, TreeItem parent, String filter) { for (Category category : categories) { TreeItem child = new TreeItem<>(category); if (category.hasSubCategories()) { createCategoryTree(category.getSubCategories(), child, filter); if (!child.getChildren().isEmpty()) { parent.getChildren().add(child); } } else if(category.contains(filter)) { parent.getChildren().add(child); } } return parent; } public void expandTree() { expandAll(categoryTree.getRoot()); } public void traverse(Consumer visitor) { for (Category category : categories) { visit(category, visitor); } } private void visit(Category cat, Consumer visitor) { for (Group group : cat.getGroups()) { for (Setting setting : group.getSettings()) { visitor.accept(setting); } } if (cat.hasSubCategories()) { for (Category subcat : cat.getSubCategories()) { visit(subcat, visitor); } } } public Optional getSetting(String key) { var search = new SettingSearchVisitor(key); traverse(search); return search.getResult(); } private class SettingSearchVisitor implements Consumer { Optional result = Optional.empty(); private String key; public SettingSearchVisitor(String key) { this.key = key; } @Override public void accept(Setting s) { if (Objects.equals(key, ofNullable(s.getKey()).orElse(""))) { result = Optional.of(s); } } public Optional getResult() { return result; } } public void onRestartRequired(Runnable callback) { this.restartRequiredCallback = callback; } public Runnable getRestartRequiredCallback() { return restartRequiredCallback; } }