diff --git a/src/main/java/jace/config/Configuration.java b/src/main/java/jace/config/Configuration.java index 43ec39d..9310aa4 100644 --- a/src/main/java/jace/config/Configuration.java +++ b/src/main/java/jace/config/Configuration.java @@ -28,6 +28,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; @@ -50,6 +51,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import javafx.collections.ObservableList; import javafx.scene.control.TreeItem; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; /** * Manages the configuration state of the emulator components. @@ -91,6 +94,11 @@ public class Configuration implements Reconfigurable { return null; } + public static ImageView getChangedIcon() { + InputStream imgStream = Configuration.class.getResourceAsStream("/jace/data/icon_exclaim.gif"); + return new ImageView(new Image(imgStream)); + } + @Override public String getName() { return "Configuration"; @@ -119,8 +127,8 @@ public class Configuration implements Reconfigurable { public transient ConfigNode parent; private ObservableList children; public transient Reconfigurable subject; - public Map settings; - public Map hotkeys; + public Map settings = new TreeMap<>(); + public Map hotkeys = new TreeMap<>();; private boolean changed = true; @Override @@ -140,8 +148,6 @@ public class Configuration implements Reconfigurable { public ConfigNode(ConfigNode parent, Reconfigurable subject) { super(); this.subject = subject; - this.settings = new TreeMap<>(); - this.hotkeys = new TreeMap<>(); this.children = getChildren(); this.parent = parent; if (this.parent != null) { @@ -151,7 +157,7 @@ public class Configuration implements Reconfigurable { } public void setFieldValue(String field, Serializable value) { - changed = true; + setChanged(true); if (value != null) { if (value.equals(getFieldValue(field))) { return; @@ -208,6 +214,15 @@ public class Configuration implements Reconfigurable { } children.add(index, newChild); } + + private void setChanged(boolean b) { + changed = b; + if (!changed) { + setGraphic(null); + } else { + setGraphic(getChangedIcon()); + } + } } public static ConfigNode BASE; public static EmulatorUILogic ui = Emulator.logic; @@ -512,7 +527,7 @@ public class Configuration implements Reconfigurable { Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); } - node.changed = false; + node.setChanged(false); } public static void applySettings(Map settings) { diff --git a/src/main/java/jace/config/ConfigurationUIController.java b/src/main/java/jace/config/ConfigurationUIController.java index 98cbc9e..1509fd2 100644 --- a/src/main/java/jace/config/ConfigurationUIController.java +++ b/src/main/java/jace/config/ConfigurationUIController.java @@ -6,30 +6,33 @@ import java.io.Serializable; import java.lang.reflect.Field; import java.net.URL; import java.util.ArrayList; -import java.util.Map; -import java.util.Map.Entry; +import java.util.HashSet; import java.util.ResourceBundle; -import java.util.function.BiConsumer; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; +import javafx.beans.Observable; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; +import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.SplitPane; import javafx.scene.control.TextField; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.util.StringConverter; public class ConfigurationUIController { + public static final String DELIMITER = "~!~"; @FXML private ResourceBundle resources; @@ -53,23 +56,25 @@ public class ConfigurationUIController { private ScrollPane treeScroll; @FXML - void reloadConfig(ActionEvent event) { - + void reloadConfig(MouseEvent event) { + Configuration.loadSettings(); + resetDeviceTree(); } @FXML - void saveConfig(ActionEvent event) { - + void saveConfig(MouseEvent event) { + Configuration.saveSettings(); } @FXML - void applyConfig(ActionEvent event) { - + void applyConfig(MouseEvent event) { + Configuration.applySettings(Configuration.BASE); + resetDeviceTree(); } @FXML - void cancelConfig(ActionEvent event) { - + void cancelConfig(MouseEvent event) { + throw new RuntimeException("Not implemented yet"); } @FXML @@ -79,11 +84,67 @@ public class ConfigurationUIController { assert settingsScroll != null : "fx:id=\"settingsScroll\" was not injected: check your FXML file 'Configuration.fxml'."; assert deviceTree != null : "fx:id=\"deviceTree\" was not injected: check your FXML file 'Configuration.fxml'."; assert treeScroll != null : "fx:id=\"treeScroll\" was not injected: check your FXML file 'Configuration.fxml'."; - deviceTree.setRoot(Configuration.BASE); + resetDeviceTree(); deviceTree.getSelectionModel().selectedItemProperty().addListener(this::selectionChanged); deviceTree.maxWidthProperty().bind(treeScroll.widthProperty()); } + + private void resetDeviceTree() { + Set expanded = new HashSet<>(); + String current = getCurrentNodePath(); + getExpandedNodes("", deviceTree.getRoot(), expanded); + deviceTree.setRoot(Configuration.BASE); + setExpandedNodes("", deviceTree.getRoot(), expanded); + setCurrentNodePath(current); + } + private void getExpandedNodes(String prefix, TreeItem root, Set expanded) { + if (root == null) return; + for (TreeItem item : root.getChildren()) { + if (item.isExpanded()) { + String name = prefix+item.toString(); + expanded.add(name); + getExpandedNodes(name+DELIMITER, item, expanded); + } + } + } + + private void setExpandedNodes(String prefix, TreeItem root, Set expanded) { + if (root == null) return; + root.getChildren().stream().forEach((item) -> { + String name = prefix+item.toString(); + if (expanded.contains(name)) { + item.setExpanded(true); + } + setExpandedNodes(name+DELIMITER, item, expanded); + }); + } + + private String getCurrentNodePath() { + TreeItem current = deviceTree.getSelectionModel().getSelectedItem(); + if (current == null) return null; + String out = current.toString(); + while (current.getParent() != null) { + out = current.getParent().toString()+DELIMITER+current; + current = current.getParent(); + } + return out; + } + + private void setCurrentNodePath(String value) { + if (value == null) return; + String[] parts = value.split(Pattern.quote(DELIMITER)); + TreeItem current = deviceTree.getRoot(); + for (String part : parts) { + for (TreeItem child : current.getChildren()) { + if (child.toString().equals(part)) { + current = child; + } + } + } + deviceTree.getSelectionModel().select(current); + } + private void selectionChanged( ObservableValue> observable, TreeItem oldValue, @@ -97,6 +158,9 @@ public class ConfigurationUIController { } private void buildForm(ConfigNode node) { + if (node == null) { + return; + } node.hotkeys.forEach((name, values) -> { settingsVbox.getChildren().add(buildKeyShortcutRow(node, name, values)); }); @@ -168,11 +232,20 @@ public class ConfigurationUIController { } private Node buildTextField(ConfigNode node, String settingName, Serializable value, String validationPattern) { - return new TextField(String.valueOf(value)); + TextField widget = new TextField(String.valueOf(value)); + widget.textProperty().addListener((e) -> { + node.setFieldValue(settingName, widget.getText()); + }); + return widget; } private Node buildBooleanField(ConfigNode node, String settingName, Serializable value) { - return new CheckBox(); + CheckBox widget = new CheckBox(); + widget.setSelected(value.equals(Boolean.TRUE)); + widget.selectedProperty().addListener((e) -> { + node.setFieldValue(settingName, widget.isSelected()); + }); + return widget; } private Node buildFileSelectionField(ConfigNode node, String settingName, Serializable value) { @@ -182,7 +255,8 @@ public class ConfigurationUIController { private Node buildDynamicSelectComponent(ConfigNode node, String settingName, Serializable value) { try { DynamicSelection sel = (DynamicSelection) node.subject.getClass().getField(settingName).get(node.subject); - ComboBox widget = new ComboBox(FXCollections.observableList(new ArrayList(sel.getSelections().keySet()))); + ChoiceBox widget = new ChoiceBox(FXCollections.observableList(new ArrayList(sel.getSelections().keySet()))); + widget.setMinWidth(175.0); widget.setConverter(new StringConverter() { @Override public String toString(Object object) { @@ -191,9 +265,18 @@ public class ConfigurationUIController { @Override public Object fromString(String string) { - return null; + return sel.findValueByMatch(string); } }); + Object selected = value == null ? null : widget.getConverter().fromString(String.valueOf(value)); + if (selected == null) { + widget.getSelectionModel().selectFirst(); + } else { + widget.setValue(selected); + } + widget.valueProperty().addListener((Observable e) -> { + node.setFieldValue(settingName, widget.getConverter().toString(widget.getValue())); + }); return widget; } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) { Logger.getLogger(ConfigurationUIController.class.getName()).log(Level.SEVERE, null, ex); diff --git a/src/main/java/jace/config/DynamicSelection.java b/src/main/java/jace/config/DynamicSelection.java index 87d916f..188e27d 100644 --- a/src/main/java/jace/config/DynamicSelection.java +++ b/src/main/java/jace/config/DynamicSelection.java @@ -45,18 +45,21 @@ public abstract class DynamicSelection implements ISelection { @Override public void setValue(T value) {currentValue = value;} + @Override public void setValueByMatch(String search) { + setValue(findValueByMatch(search)); + } + + public T findValueByMatch(String search) { Map selections = getSelections(); String match = Utility.findBestMatch(search, selections.values()); if (match != null) { for (T key : selections.keySet()) { if (selections.get(key).equals(match)) { - setValue(key); - return; + return key; } } - } else { - setValue(null); } + return null; } } diff --git a/src/main/resources/jace/data/icon_exclaim.gif b/src/main/resources/jace/data/icon_exclaim.gif new file mode 100644 index 0000000..215b32e Binary files /dev/null and b/src/main/resources/jace/data/icon_exclaim.gif differ