Repository: mzmine/mzmine3
Issue: [Bug] Ion Identity: Cannot import custom adducts/modifications list from csv

### Basic information

* My operating system: MacOS 12.3
* My MZmine version: 3.0.0-beta

### What happened

* Please describe what happened and how it differs from what you expected to happen.

I tried importing a custom adducts.csv file but it does not load desired adducts.

* Please include all parameter values for the MZmine modules you used. 

No parameters used, just did: `Feature list methods > Feature grouping > Add more ion identities`

You can try exporting the default list and re-importing while adding a single dummy line it does not work

* Please upload screenshots of any errors and your log/mzmine.log file.

```
2022-03-07 18:24:51 WARNING io.github.mzmine.parameters.parametertypes.MultiChoiceComponent loadChoices Line length is different to header length: 5 to 5
```

* Please upload an MZmine project file that demonstrates the problem you are reporting.

NA

Changed files:
src/main/java/io/github/mzmine/parameters/parametertypes/MultiChoiceComponent.java
src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationComponent.java
src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationParameter.java

diff --git a/src/main/java/io/github/mzmine/parameters/parametertypes/MultiChoiceComponent.java b/src/main/java/io/github/mzmine/parameters/parametertypes/MultiChoiceComponent.java
index d361e3ec4..803a86a59 100644
--- a/src/main/java/io/github/mzmine/parameters/parametertypes/MultiChoiceComponent.java
+++ b/src/main/java/io/github/mzmine/parameters/parametertypes/MultiChoiceComponent.java
@@ -1,19 +1,19 @@
 /*
- * Copyright 2006-2018 The MZmine 2 Development Team
- * 
- * This file is part of MZmine 2.
- * 
- * MZmine 2 is free software; you can redistribute it and/or modify it under the terms of the GNU
+ * Copyright 2006-2021 The MZmine Development Team
+ *
+ * This file is part of MZmine.
+ *
+ * MZmine is free software; you can redistribute it and/or modify it under the terms of the GNU
  * General Public License as published by the Free Software Foundation; either version 2 of the
  * License, or (at your option) any later version.
- * 
- * MZmine 2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
- * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ *
+ * MZmine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  * General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License along with MZmine 2; if not,
- * write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
- * USA
+ *
+ * You should have received a copy of the GNU General Public License along with MZmine; if not,
+ * write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
  */
 
 package io.github.mzmine.parameters.parametertypes;
@@ -25,7 +25,19 @@ import io.github.mzmine.main.MZmineCore;
 import io.github.mzmine.parameters.ParameterSet;
 import io.github.mzmine.util.ExitCode;
 import io.github.mzmine.util.StringMapParser;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 import javafx.event.ActionEvent;
@@ -37,37 +49,23 @@ import javafx.scene.layout.BorderPane;
 import javafx.scene.layout.VBox;
 import javafx.stage.FileChooser;
 import org.controlsfx.control.CheckListView;
-
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.util.*;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import org.controlsfx.control.IndexedCheckModel;
 
 /**
+ *
  */
 public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPane {
-  private final Logger logger = Logger.getLogger(getClass().getName());
-  // Filename extension.
-  private static final FileChooser.ExtensionFilter csvFilter =
-          new FileChooser.ExtensionFilter("Comma-separated values files", "*.csv");
 
-  private final CheckListView<T> adductsView =
-          new CheckListView<>(FXCollections.observableArrayList());
+  // Filename extension.
+  private static final FileChooser.ExtensionFilter csvFilter = new FileChooser.ExtensionFilter(
+      "Comma-separated values files", "*.csv");
+  private final Logger logger = Logger.getLogger(getClass().getName());
+  private final CheckListView<T> adductsView = new CheckListView<>(
+      FXCollections.observableArrayList());
 
   private final Label lbTitle;
-  private final Button addButton = new Button("Add...");
-  private final Button importButton = new Button("Import...");
-  private final Button exportButton = new Button("Export...");
-  private final Button defaultButton = new Button("Reset");
   private final VBox buttonBar = new VBox();
-
-  private final List<T> defaultChoices;
-
-  private StringMapParser<T> parser;
-
+  private final StringMapParser<T> parser;
 
   /**
    * Create the component.
@@ -75,45 +73,79 @@ public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPa
    * @param choices the adduct choices.
    */
   public MultiChoiceComponent(List<T> choices, List<T> defaultChoices, Supplier<T> addChoiceParam,
-                              StringMapParser<T> parser) {
-    this(choices, defaultChoices, addChoiceParam, parser, true, true, true, true);
+      StringMapParser<T> parser) {
+    this(choices, defaultChoices, addChoiceParam, parser, true, true, true, true, true, true);
   }
+
   /**
    * Create the component.
    *
-   * @param choices the adduct choices.
+   * @param choices        the adduct choices.
    * @param addChoiceParam usually a ParameterSet as ObjectGenerator to add new choices
    */
-  public MultiChoiceComponent(List<T> choices, List<T> defaultChoices, Supplier<T> addChoiceParam, StringMapParser<T> parser,
-                              boolean btnAdd, boolean btnImport, boolean btnExport, boolean btnDefault) {
-    this.defaultChoices = defaultChoices;
+  public MultiChoiceComponent(List<T> choices, List<T> defaultChoices, Supplier<T> addChoiceParam,
+      StringMapParser<T> parser, boolean btnToggleSelect, boolean btnClear, boolean btnAdd,
+      boolean btnImport, boolean btnExport, boolean btnDefault) {
     this.parser = parser;
 
     setChoices(choices);
 
     buttonBar.setSpacing(10.0);
-    if(btnAdd)
-      buttonBar.getChildren().addAll(addButton);
-    if(btnImport)
+    Button importButton = new Button("Import...");
+    if (btnImport) {
       buttonBar.getChildren().addAll(importButton);
-    if(btnExport)
+    }
+    Button exportButton = new Button("Export...");
+    if (btnExport) {
       buttonBar.getChildren().addAll(exportButton);
-    if(btnDefault)
+    }
+    Button toggleSelectButton = new Button("(De)select");
+    if (btnToggleSelect) {
+      buttonBar.getChildren().addAll(toggleSelectButton);
+    }
+    Button clearButton = new Button("Clear");
+    if (btnClear) {
+      buttonBar.getChildren().addAll(clearButton);
+    }
+    Button addButton = new Button("Add...");
+    if (btnAdd) {
+      buttonBar.getChildren().addAll(addButton);
+    }
+    Button defaultButton = new Button("Reset");
+    if (btnDefault) {
       buttonBar.getChildren().addAll(defaultButton);
+    }
 
     lbTitle = new Label("");
     setTop(lbTitle);
     setCenter(adductsView);
     setRight(buttonBar);
 
+    clearButton.setTooltip(new Tooltip("Remove all items"));
+    clearButton.setOnAction(e -> adductsView.getItems().clear());
+
+    toggleSelectButton.setTooltip(new Tooltip("Toggle selection"));
+    toggleSelectButton.setOnAction(e -> {
+      final IndexedCheckModel<T> model = adductsView.getCheckModel();
+      final ObservableList<T> items = adductsView.getItems();
+      if (items.size() > 0) {
+        boolean newState = !model.isChecked(items.get(0));
+        model.clearChecks();
+        if (newState) {
+          for (T adduct : items) {
+            model.check(adduct);
+          }
+        }
+      }
+    });
+
     addButton.setTooltip(new Tooltip("Add a custom choice to the set of choices"));
     addButton.setOnAction(e -> {
-      if(addChoiceParam==null) {
+      if (addChoiceParam == null) {
         return;
       }
       // Show dialog.
-      if(addChoiceParam instanceof ParameterSet) {
-        final ParameterSet parameters = ((ParameterSet)addChoiceParam);
+      if (addChoiceParam instanceof final ParameterSet parameters) {
         if (parameters.showSetupDialog(true) == ExitCode.OK) {
           // Add to list of choices (if not already present).
           T choice = addChoiceParam.get();
@@ -122,15 +154,14 @@ public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPa
             currentChoices.add(choice);
           }
         }
-      }
-      else {
+      } else {
         try {
           T choice = addChoiceParam.get();
           final Collection<T> currentChoices = adductsView.getItems();
           if (!currentChoices.contains(choice)) {
             currentChoices.add(choice);
           }
-        } catch(Exception ex) {
+        } catch (Exception ex) {
           logger.warning("Cannot create new choice");
         }
       }
@@ -145,8 +176,9 @@ public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPa
       final File file = fileChooser.showOpenDialog(this.getScene().getWindow());
 
       // Select a file.
-      if (file == null)
+      if (file == null) {
         return;
+      }
 
       // Read the CSV file into a string array.
       String[][] csvLines = null;
@@ -159,8 +191,9 @@ public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPa
       }
 
       // Read the adducts data.
-      if (csvLines == null)
+      if (csvLines == null) {
         return;
+      }
 
       // Load adducts from CSV data into parent choices.
       loadChoices(csvLines, adductsView.getItems());
@@ -174,8 +207,9 @@ public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPa
       fileChooser.getExtensionFilters().add(csvFilter);
       final File file = fileChooser.showSaveDialog(this.getScene().getWindow());
 
-      if (file == null)
+      if (file == null) {
         return;
+      }
 
       // Export the adducts.
       try {
@@ -199,14 +233,26 @@ public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPa
     return new ArrayList<>(adductsView.getItems());
   }
 
+  public void setChoices(List<T> choices) {
+    adductsView.getItems().clear();
+    adductsView.getItems().addAll(choices);
+  }
+
+  @SafeVarargs
+  public final void setChoices(T... choices) {
+    adductsView.getItems().clear();
+    adductsView.getItems().addAll(choices);
+  }
+
   public List<T> getValue() {
     return new ArrayList<>(adductsView.getCheckModel().getCheckedItems());
   }
 
   public void setValue(List<T> newValue) {
-    adductsView.getCheckModel().clearChecks();;
-    for (T adduct : newValue)
+    adductsView.getCheckModel().clearChecks();
+    for (T adduct : newValue) {
       adductsView.getCheckModel().check(adduct);
+    }
   }
 
   public String getTitle() {
@@ -230,46 +276,47 @@ public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPa
   /**
    * Load the adducts into the list of adduct choices.
    *
-   * @param lines CSV lines to parse.
+   * @param lines   CSV lines to parse.
    * @param choices the current adduct choices.
    * @return a new list of adduct choices that includes the original choices plus any new ones found
-   *         by parsing the CSV lines.
+   * by parsing the CSV lines.
    */
   private void loadChoices(final String[][] lines, final Collection<T> choices) {
-    if(lines.length<2)
+    if (lines.length < 2) {
       return;
+    }
     // load a list of choices.
     String[] header = lines[0];
-    for (int l=1; l<lines.length; l++) {
+    for (int l = 1; l < lines.length; l++) {
       String[] line = lines[l];
-      if(lines.length!=header.length) {
-        logger.warning("Line length is different to header length: "+line.length+" to "+header.length);
+      if (line.length != header.length) {
+        logger.warning(
+            "Line length is different to header length: " + line.length + " to " + header.length);
         continue;
       }
       // map with values
-      Map<String,String> map = new HashMap<>();
-      for(int f=0; f<line.length; f++) {
+      Map<String, String> map = new HashMap<>();
+      for (int f = 0; f < line.length; f++) {
         map.put(header[f], line[f]);
       }
       try {
         // Create new choice
-        if(parser!=null) {
+        if (parser != null) {
           final T choice = parser.parseDataMap(map);
           if (!choices.contains(choice)) {
             choices.add(choice);
           }
         }
       } catch (Exception ignored) {
-        logger.warning("Cannot parse new choice: "+line);
+        logger.warning("Cannot parse new choice: " + Arrays.toString(line));
       }
     }
   }
 
-
   /**
    * Writes the choices to a CSV file.
    *
-   * @param file the destination file.
+   * @param file    the destination file.
    * @param choices the choices to export.
    * @throws IOException if there are i/o problems.
    */
@@ -287,13 +334,4 @@ public class MultiChoiceComponent<T extends StringMapParser<T>> extends BorderPa
       writer.writeln(map.values().toArray(String[]::new));
     }
   }
-
-  public void setChoices(List<T> choices) {
-    adductsView.getItems().clear();
-    adductsView.getItems().addAll(choices);
-  }
-  public void setChoices(T... choices) {
-    adductsView.getItems().clear();
-    adductsView.getItems().addAll(choices);
-  }
 }
diff --git a/src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationComponent.java b/src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationComponent.java
index 92f907b48..e09a27c41 100644
--- a/src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationComponent.java
+++ b/src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationComponent.java
@@ -30,7 +30,6 @@ import javafx.scene.layout.HBox;
 
 /**
  * A component for selecting adducts.
- *
  */
 public class IonModificationComponent extends HBox {
 
@@ -42,26 +41,29 @@ public class IonModificationComponent extends HBox {
    *
    * @param choicesAdducts the adduct choices.
    */
-  public IonModificationComponent(List<IonModification> choicesAdducts, List<IonModification> choicesMods) {
+  public IonModificationComponent(List<IonModification> choicesAdducts,
+      List<IonModification> choicesMods) {
     setFillHeight(true);
     setSpacing(5);
 
-    adducts = new MultiChoiceComponent<>(choicesAdducts, List.of(IonModification.getDefaultValuesPos()), null,
-            IonModification.H, // just any object as a parser
-        false, true, true, false
-    );
+    adducts = new MultiChoiceComponent<>(choicesAdducts,
+        List.of(IonModification.getDefaultValuesPos()), null, IonModification.H,
+        // just any object as a parser
+        true, true, false, true, true, false);
     // add top label
     adducts.setTitle("Adducts");
     // add buttons
     adducts.addButton("Add", new AddIonModificationAction(adducts));
     adducts.addButton("Combine", new CombineESIAdductsAction(adducts));
-    adducts.addButton("Reset positive", (e) -> adducts.setChoices(IonModification.getDefaultValuesPos()));
-    adducts.addButton("Reset negative", (e) -> adducts.setChoices(IonModification.getDefaultValuesNeg()));
+    adducts.addButton("Reset positive",
+        (e) -> adducts.setChoices(IonModification.getDefaultValuesPos()));
+    adducts.addButton("Reset negative",
+        (e) -> adducts.setChoices(IonModification.getDefaultValuesNeg()));
 
-    mods = new MultiChoiceComponent<IonModification>(choicesMods, List.of(IonModification.getDefaultModifications()), null,
-            IonModification.H, // just any object as a parser
-        false, true, true, false
-    );
+    mods = new MultiChoiceComponent<>(choicesMods,
+        List.of(IonModification.getDefaultModifications()), null, IonModification.H,
+        // just any object as a parser
+        true, true, false, true, true, false);
     // add top label
     mods.setTitle("Modifications");
     // add buttons
@@ -75,8 +77,7 @@ public class IonModificationComponent extends HBox {
   public IonModification[][] getChoices() {
     IonModification[] ad = adducts.getChoices().toArray(IonModification[]::new);
     IonModification[] md = mods.getChoices().toArray(IonModification[]::new);
-    IonModification[][] all = {ad, md};
-    return all;
+    return new IonModification[][]{ad, md};
   }
 
   /**
@@ -89,8 +90,7 @@ public class IonModificationComponent extends HBox {
         .toArray(IonModification[]::new);
     IonModification[] md = mods.getValue().stream().filter(Objects::nonNull)
         .toArray(IonModification[]::new);
-    IonModification[][] all = {ad, md};
-    return all;
+    return new IonModification[][]{ad, md};
   }
 
   public void setValue(final IonModification[][] values) {
diff --git a/src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationParameter.java b/src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationParameter.java
index 97a87a5bd..41cf5a456 100644
--- a/src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationParameter.java
+++ b/src/main/java/io/github/mzmine/parameters/parametertypes/ionidentity/IonModificationParameter.java
@@ -16,30 +16,6 @@
  *
  */
 
-/*
- * Copyright 2006-2015 The MZmine 2 Development Team
- *
- * This file is part of MZmine 2.
- *
- * MZmine 2 is free software; you can redistribute it and/or modify it under the terms of the GNU
- * General Public License as published by the Free Software Foundation; either version 2 of the
- * License, or (at your option) any later version.
- *
- * MZmine 2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
- * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with MZmine 2; if not,
- * write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
- * USA
- */
-
-/*
- * Code created was by or on behalf of Syngenta and is released under the open source license in use
- * for the pre-existing code or project. Syngenta does not assert ownership or copyright any over
- * pre-existing work.
- */
-
 package io.github.mzmine.parameters.parametertypes.ionidentity;
 
 import io.github.mzmine.datamodel.identities.iontype.CombinedIonModification;
@@ -47,13 +23,16 @@ import io.github.mzmine.datamodel.identities.iontype.IonModification;
 import io.github.mzmine.datamodel.identities.iontype.IonModificationType;
 import io.github.mzmine.parameters.UserParameter;
 import io.github.mzmine.parameters.parametertypes.MultiChoiceParameter;
-import org.w3c.dom.*;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.logging.Logger;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 
 /**
  * Adducts parameter.
@@ -65,7 +44,7 @@ public class IonModificationParameter
     implements UserParameter<IonModification[][], IonModificationComponent> {
 
   // Logger.
-  private static final Logger LOG = Logger.getLogger(IonModificationParameter.class.getName());
+  private static final Logger logger = Logger.getLogger(IonModificationParameter.class.getName());
 
   // XML tags.
   private static final String MODIFICTAION_TAG = "modification_type";
@@ -165,7 +144,8 @@ public class IonModificationParameter
                 adducts.add(add);
               } catch (NumberFormatException ex) {
                 // Ignore.
-                LOG.warning("Illegal mass difference attribute in " + childAdduct.getNodeValue());
+                logger.warning(
+                    "Illegal mass difference attribute in " + childAdduct.getNodeValue());
               }
             }
           }