From 3f1fbadc4a406ebcd67addaaa848d8fae7627de0 Mon Sep 17 00:00:00 2001
From: Tim Koers <tim.koers@live.nl>
Date: Mon, 19 Mar 2018 21:37:43 +0100
Subject: [PATCH 1/4] First version of the auto board selector.

Working!
---
 app/src/processing/app/Base.java            |   6 +-
 app/src/processing/app/Editor.java          |  17 ++++
 app/src/processing/app/EditorTab.java       |  49 +++++++--
 arduino-core/src/processing/app/Sketch.java | 105 ++++++++++++++++++++
 4 files changed, 169 insertions(+), 8 deletions(-)

diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index 1752e1dc824..468594c4f30 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -477,7 +477,10 @@ public Base(String[] args) throws Exception {
         contributionsSelfCheck = new ContributionsSelfCheck(this, new UpdatableBoardsLibsFakeURLsHandler(this), contributionInstaller, libraryInstaller);
         new Timer(false).schedule(contributionsSelfCheck, Constants.BOARDS_LIBS_UPDATABLE_CHECK_START_PERIOD);
       }
-
+      // Load the build settings
+      for(Editor editor: editors){
+        editor.findTab(editor.sketch.getPrimaryFile()).loadBuildSettings(this);
+      }
     } else if (parser.isNoOpMode()) {
       // Do nothing (intended for only changing preferences)
       System.exit(0);
@@ -1496,6 +1499,7 @@ public void actionPerformed(ActionEvent actionevent) {
     }
   }
 
+
   private JRadioButtonMenuItem createBoardMenusAndCustomMenus(
           final List<JMenu> boardsCustomMenus, List<JMenuItem> menuItemsToClickAfterStartup,
           Map<String, ButtonGroup> buttonGroupsMap,
diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java
index 99e53e488dc..102d652c737 100644
--- a/app/src/processing/app/Editor.java
+++ b/app/src/processing/app/Editor.java
@@ -65,6 +65,7 @@
 import java.util.*;
 import java.util.List;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import java.util.ArrayList;
@@ -339,6 +340,7 @@ public void windowDeactivated(WindowEvent e) {
     // Open the document that was passed in
     boolean loaded = handleOpenInternal(file);
     if (!loaded) sketchController = null;
+
   }
 
 
@@ -764,6 +766,10 @@ private JMenu buildToolsMenu() {
     item = new JMenuItem(tr("Get Board Info"));
     item.addActionListener(e -> handleBoardInfo());
     toolsMenu.add(item);
+
+    item = new JMenuItem(tr("Add build settings to .INO file"));
+    item.addActionListener(e -> handleAddBuildSettings());
+    toolsMenu.add(item);
     toolsMenu.addSeparator();
 
     base.rebuildProgrammerMenu();
@@ -2512,6 +2518,17 @@ public void handlePlotter() {
 
   }
 
+  public void handleAddBuildSettings(){
+    final LinkedHashMap<String, String> settingsMap = base.getBoardsCustomMenus().stream().filter(JMenu::isVisible).map((e)->{
+      String setting = e.getText().substring(0, e.getText().indexOf(":"));
+      String value = e.getText().replace(setting + ":", "").replace("\"", "").trim();
+      return new String[]{setting, value};
+    }).collect(LinkedHashMap::new, (map, menu) -> map.put(menu[0], menu[1]), LinkedHashMap::putAll);
+    sketch.setBuildSettings(findTab(sketch.getPrimaryFile()), settingsMap);
+    handleSave(true);
+    System.out.println("Build settings header should be added");
+  }
+
   private void handleBurnBootloader() {
     console.clear();
     statusNotice(tr("Burning bootloader to I/O Board (this may take a minute)..."));
diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java
index 33dabdbbbc2..b190d9b6e34 100644
--- a/app/src/processing/app/EditorTab.java
+++ b/app/src/processing/app/EditorTab.java
@@ -34,13 +34,12 @@
 import java.awt.event.MouseWheelEvent;
 
 import java.io.IOException;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Optional;
 
-import javax.swing.Action;
-import javax.swing.BorderFactory;
-import javax.swing.JMenuItem;
-import javax.swing.JPanel;
-import javax.swing.JPopupMenu;
-import javax.swing.ToolTipManager;
+import javax.swing.*;
 import javax.swing.border.MatteBorder;
 import javax.swing.event.PopupMenuEvent;
 import javax.swing.event.PopupMenuListener;
@@ -57,6 +56,8 @@
 import org.fife.ui.rtextarea.RTextScrollPane;
 
 import cc.arduino.UpdatableBoardsLibsFakeURLsHandler;
+import processing.app.debug.TargetBoard;
+import processing.app.debug.TargetPackage;
 import processing.app.helpers.DocumentTextChangeListener;
 import processing.app.syntax.ArduinoTokenMakerFactory;
 import processing.app.syntax.PdeKeywords;
@@ -109,7 +110,7 @@ public EditorTab(Editor editor, SketchFile file, String contents)
     file.setStorage(this);
     applyPreferences();
     add(scrollPane, BorderLayout.CENTER);
-	textarea.addMouseWheelListener(this);
+	  textarea.addMouseWheelListener(this);
   }
 
   private RSyntaxDocument createDocument(String contents) {
@@ -433,6 +434,40 @@ public void setText(String what) {
     }
   }
 
+  /**
+   * This method loads the build settings from the main .INO file and sets the Arduino IDE accordingly
+   */
+  public void loadBuildSettings(Base base){
+
+    LinkedHashMap<String, String> buildSettings = getSketch().getSketch().getBuildSettingsFromProgram(getText());
+
+    Optional<TargetBoard> optionalTargetBoard = BaseNoGui.getTargetPlatform().getBoards().values().stream().filter(board -> board.getPreferences().get("name","").equals("Node32s")).findFirst();
+
+    TargetBoard targetBoard;
+    if(optionalTargetBoard.isPresent()){
+      targetBoard = optionalTargetBoard.get();
+    }else{
+      return;
+    }
+
+    for(String readableName : buildSettings.values()){
+      base.getBoardsCustomMenus().forEach(menuItem -> {
+        Optional<JRadioButtonMenuItem> optionalBoardMenuItem = Arrays.stream(menuItem.getMenuComponents()).filter(subItem->{
+          return subItem instanceof JRadioButtonMenuItem && ((JRadioButtonMenuItem)subItem).getText().equals(readableName) && (((JRadioButtonMenuItem) subItem).getAction().getValue("board") == null || (((JRadioButtonMenuItem) subItem).getAction().getValue("board").equals(targetBoard)));
+          }
+        ).map(subItem-> {
+          return subItem instanceof JRadioButtonMenuItem ? (JRadioButtonMenuItem)subItem : new JRadioButtonMenuItem();
+        }).findFirst();
+        if(optionalBoardMenuItem.isPresent()){
+          optionalBoardMenuItem.get().setSelected(true);
+          optionalBoardMenuItem.get().getAction().actionPerformed(new ActionEvent(this, -1, ""));
+        }else{
+          // TODO Ask the user which value should replace the current value
+        }
+      });
+    }
+  }
+
   /**
    * Is the text modified since the last save / load?
    */
diff --git a/arduino-core/src/processing/app/Sketch.java b/arduino-core/src/processing/app/Sketch.java
index 6c417403ec9..c7ee9c9cbea 100644
--- a/arduino-core/src/processing/app/Sketch.java
+++ b/arduino-core/src/processing/app/Sketch.java
@@ -139,6 +139,111 @@ public void save() throws IOException {
     }
   }
 
+  private final String buildToolsHeader = "\n/** Arduino IDE Board Tool details\n";
+  private final String buildToolsHeaderEnd = "*/";
+
+  /**
+   * Checks the code for a valid build tool header
+   * @param program The code to scan for the build tools
+   * @return True if the build tool header was found ONE time. Returns false if found MORE than one time, or not found at all.
+   */
+  private boolean containsBuildSettings(String program){
+    return program.contains(buildToolsHeader) && (program.indexOf(buildToolsHeader) == program.lastIndexOf(buildToolsHeader));
+  }
+
+  /**
+   * This function returns the index of the Nth occurrence of the substring in the specified string (http://programming.guide/java/nth-occurrence-in-string.html)
+   * @param str The string to find the Nth occurrence in
+   * @param substr The string to find
+   * @param n The occurrence number you'd like to find
+   * @return
+   */
+  private static int ordinalIndexOf(String str, String substr, int n) {
+    int pos = str.indexOf(substr);
+    while (--n > 0 && pos != -1)
+      pos = str.indexOf(substr, pos + 1);
+    return pos;
+  }
+
+  private void removeBuildSettingsHeader(EditorTab tab){
+    if(tab.getText().contains(buildToolsHeader)) {
+      int headerStartIndex = tab.getText().indexOf(buildToolsHeader);
+      int headerStopIndex = tab.getText().indexOf(buildToolsHeaderEnd);
+      if (headerStartIndex > headerStopIndex) {
+        System.err.println("The build tool header is not the first comment block in your file! Please fix this.");
+        for (int i = 0; i < tab.getText().length(); i++) {
+          if (headerStartIndex < ordinalIndexOf(tab.getText(), buildToolsHeaderEnd, i)) {
+            headerStopIndex = ordinalIndexOf(tab.getText(), buildToolsHeaderEnd, i);
+            break;
+          }
+        }
+      }
+      String header = tab.getText().substring(headerStartIndex, headerStopIndex + buildToolsHeaderEnd.length());
+      tab.setText(tab.getText().replace(header, ""));
+      // Run this method again so we are sure that there aren't any headers left
+      removeBuildSettingsHeader(tab);
+    }
+  }
+
+  /**
+   * This checks the program code for a valid build tool settings header and returns the LinkedHashMap with the setting name and the value.
+   * The build tools header should not be changed or manipulated by the pre-processor as the pre-processors output may depend on the build tools.
+   * @param program The program code
+   * @return The {@code LinkedHashMap} with the settings and their values of the <b>first</b> header that was found in the program code
+   */
+  public LinkedHashMap<String, String> getBuildSettingsFromProgram(String program){
+    LinkedHashMap<String, String> buildSettings = new LinkedHashMap<>();
+    if(containsBuildSettings(program)){
+        int headerStartIndex = program.indexOf(buildToolsHeader);
+        int headerStopIndex = program.indexOf(buildToolsHeaderEnd);
+        if(headerStartIndex > headerStopIndex){
+          System.err.println("The build tool header is not the first comment block in your file! Please fix this.");
+          for(int i = 0; i < program.length(); i++){
+            if(headerStartIndex < ordinalIndexOf(program, buildToolsHeaderEnd, i)){
+              headerStopIndex = ordinalIndexOf(program, buildToolsHeaderEnd, i);
+              break;
+            }
+          }
+        }
+        String header = program.substring(headerStartIndex + buildToolsHeader.length(), headerStopIndex);
+
+        String[] headerLines = header.split("\n");
+
+        for(int line = 0; line < headerLines.length; line++){
+          String[] setting = headerLines[line].replace("*","").trim().split(": ");
+          if(headerLines[line].indexOf(": ") != (headerLines[line].length() -1)){
+            // The value of the setting is not empty
+            buildSettings.put(setting[0].trim(), setting[1].trim());
+          }else{
+            buildSettings.put(setting[0], "");
+          }
+        }
+    }else{
+      if(!program.contains(buildToolsHeader)){
+        // There are multiple headers, remove them
+        // TODO Create a dialog asking the user to add a build header to the file
+      }
+    }
+
+    return buildSettings;
+  }
+
+  private boolean isBuildSettingsEqual(LinkedHashMap<String,String> first, LinkedHashMap<String, String> second){
+    return first.keySet().containsAll(second.keySet()) && first.values().containsAll(second.values());
+  }
+
+  public void setBuildSettings(EditorTab tab, LinkedHashMap<String, String> buildSettings){
+    if(tab.getSketch().getSketch() != this){
+      return;
+    }
+
+    String customBoardSettingsHeader = buildSettings.entrySet().stream().map(entry-> String.format(" * %s: %s\n", entry.getKey(), entry.getValue())).collect(Collectors.joining("", buildToolsHeader, "*/"));
+    if(!isBuildSettingsEqual(getBuildSettingsFromProgram(tab.getText()),buildSettings)){
+      removeBuildSettingsHeader(tab);
+      tab.setText(customBoardSettingsHeader + ((tab.getText().charAt(0) == '\n') ? "" : "\n") + tab.getText());
+    }
+  }
+
   public int getCodeCount() {
     return files.size();
   }

From 0b2982c73802cca30f96a3434f9dc557471f86d6 Mon Sep 17 00:00:00 2001
From: Tim Koers <tim.koers@live.nl>
Date: Tue, 20 Mar 2018 15:06:01 +0100
Subject: [PATCH 2/4] Little formatting fix and added the Arduino/Genuino Uno
 as the default header for the sketch

---
 .gitignore                                                  | 1 +
 app/src/processing/app/Base.java                            | 2 --
 app/src/processing/app/EditorTab.java                       | 1 +
 build/shared/examples/01.Basics/BareMinimum/BareMinimum.ino | 6 +++++-
 4 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore
index 0ff213c4047..5f5d747e730 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ app/pde.jar
 build/macosx/work/
 arduino-core/bin/
 arduino-core/arduino-core.jar
+lib/*
 hardware/arduino/bootloaders/caterina_LUFA/Descriptors.o
 hardware/arduino/bootloaders/caterina_LUFA/Descriptors.lst
 hardware/arduino/bootloaders/caterina_LUFA/Caterina.sym
diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index 468594c4f30..5cfb8dc0f4c 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -680,10 +680,8 @@ protected int[] nextEditorLocation() {
     }
   }
 
-
   // .................................................................
 
-
   boolean breakTime = false;
   String[] months = {
           "jan", "feb", "mar", "apr", "may", "jun",
diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java
index b190d9b6e34..54d17f3a9e3 100644
--- a/app/src/processing/app/EditorTab.java
+++ b/app/src/processing/app/EditorTab.java
@@ -463,6 +463,7 @@ public void loadBuildSettings(Base base){
           optionalBoardMenuItem.get().getAction().actionPerformed(new ActionEvent(this, -1, ""));
         }else{
           // TODO Ask the user which value should replace the current value
+
         }
       });
     }
diff --git a/build/shared/examples/01.Basics/BareMinimum/BareMinimum.ino b/build/shared/examples/01.Basics/BareMinimum/BareMinimum.ino
index 95c2b6eb0a8..6586e6e69ab 100644
--- a/build/shared/examples/01.Basics/BareMinimum/BareMinimum.ino
+++ b/build/shared/examples/01.Basics/BareMinimum/BareMinimum.ino
@@ -1,3 +1,7 @@
+/** Arduino IDE Board Tool details
+ * Board: Arduino/Genuino Uno
+*/
+
 void setup() {
   // put your setup code here, to run once:
 
@@ -6,4 +10,4 @@ void setup() {
 void loop() {
   // put your main code here, to run repeatedly:
 
-}
+}
\ No newline at end of file

From 18b401660d804f1f9cde7ff1aa2302b1264d466b Mon Sep 17 00:00:00 2001
From: timkoers <timkoers@users.noreply.github.com>
Date: Tue, 20 Mar 2018 16:40:54 +0100
Subject: [PATCH 3/4] Update Sketch.java

This should fix the build error
---
 arduino-core/src/processing/app/Sketch.java | 1 +
 1 file changed, 1 insertion(+)

diff --git a/arduino-core/src/processing/app/Sketch.java b/arduino-core/src/processing/app/Sketch.java
index c7ee9c9cbea..8c02ba50758 100644
--- a/arduino-core/src/processing/app/Sketch.java
+++ b/arduino-core/src/processing/app/Sketch.java
@@ -9,6 +9,7 @@
 
 import cc.arduino.files.DeleteFilesOnShutdown;
 import processing.app.helpers.FileUtils;
+import processing.app.EditorTab;
 
 import static processing.app.I18n.tr;
 

From 9f1dcee2c58c6aea5732b365486125e2a215c3a0 Mon Sep 17 00:00:00 2001
From: Tim Koers <tim.koers@live.nl>
Date: Tue, 20 Mar 2018 19:08:59 +0100
Subject: [PATCH 4/4] The Java compilation error has been fixed

---
 app/src/processing/app/Editor.java          | 10 ++++--
 app/src/processing/app/EditorTab.java       |  1 +
 arduino-core/src/processing/app/Sketch.java | 35 ++++++++++-----------
 3 files changed, 25 insertions(+), 21 deletions(-)

diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java
index 102d652c737..cd9c49e9382 100644
--- a/app/src/processing/app/Editor.java
+++ b/app/src/processing/app/Editor.java
@@ -2524,9 +2524,13 @@ public void handleAddBuildSettings(){
       String value = e.getText().replace(setting + ":", "").replace("\"", "").trim();
       return new String[]{setting, value};
     }).collect(LinkedHashMap::new, (map, menu) -> map.put(menu[0], menu[1]), LinkedHashMap::putAll);
-    sketch.setBuildSettings(findTab(sketch.getPrimaryFile()), settingsMap);
-    handleSave(true);
-    System.out.println("Build settings header should be added");
+	handleSave(true);
+    Optional<EditorTab> optionalEditorTab = tabs.stream().filter(tab -> tab.getSketch().getSketch().equals(sketch)).findFirst();
+    if(optionalEditorTab.isPresent()){
+      optionalEditorTab.get().setText(sketch.setBuildSettings(sketch, settingsMap));
+      handleSave(true);
+      System.out.println("Build settings header should be added");
+    }
   }
 
   private void handleBurnBootloader() {
diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java
index 54d17f3a9e3..07ac0020aa0 100644
--- a/app/src/processing/app/EditorTab.java
+++ b/app/src/processing/app/EditorTab.java
@@ -434,6 +434,7 @@ public void setText(String what) {
     }
   }
 
+
   /**
    * This method loads the build settings from the main .INO file and sets the Arduino IDE accordingly
    */
diff --git a/arduino-core/src/processing/app/Sketch.java b/arduino-core/src/processing/app/Sketch.java
index 8c02ba50758..c9c42bc7f59 100644
--- a/arduino-core/src/processing/app/Sketch.java
+++ b/arduino-core/src/processing/app/Sketch.java
@@ -9,7 +9,6 @@
 
 import cc.arduino.files.DeleteFilesOnShutdown;
 import processing.app.helpers.FileUtils;
-import processing.app.EditorTab;
 
 import static processing.app.I18n.tr;
 
@@ -166,24 +165,23 @@ private static int ordinalIndexOf(String str, String substr, int n) {
     return pos;
   }
 
-  private void removeBuildSettingsHeader(EditorTab tab){
-    if(tab.getText().contains(buildToolsHeader)) {
-      int headerStartIndex = tab.getText().indexOf(buildToolsHeader);
-      int headerStopIndex = tab.getText().indexOf(buildToolsHeaderEnd);
+  private String removeBuildSettingsHeader(Sketch sketch){
+    if(sketch.getPrimaryFile().getProgram().contains(buildToolsHeader)) {
+      int headerStartIndex = sketch.getPrimaryFile().getProgram().indexOf(buildToolsHeader);
+      int headerStopIndex = sketch.getPrimaryFile().getProgram().indexOf(buildToolsHeaderEnd);
       if (headerStartIndex > headerStopIndex) {
         System.err.println("The build tool header is not the first comment block in your file! Please fix this.");
-        for (int i = 0; i < tab.getText().length(); i++) {
-          if (headerStartIndex < ordinalIndexOf(tab.getText(), buildToolsHeaderEnd, i)) {
-            headerStopIndex = ordinalIndexOf(tab.getText(), buildToolsHeaderEnd, i);
+        for (int i = 0; i < sketch.getPrimaryFile().getProgram().length(); i++) {
+          if (headerStartIndex < ordinalIndexOf(sketch.getPrimaryFile().getProgram(), buildToolsHeaderEnd, i)) {
+            headerStopIndex = ordinalIndexOf(sketch.getPrimaryFile().getProgram(), buildToolsHeaderEnd, i);
             break;
           }
         }
       }
-      String header = tab.getText().substring(headerStartIndex, headerStopIndex + buildToolsHeaderEnd.length());
-      tab.setText(tab.getText().replace(header, ""));
-      // Run this method again so we are sure that there aren't any headers left
-      removeBuildSettingsHeader(tab);
+      String header = sketch.getPrimaryFile().getProgram().substring(headerStartIndex, headerStopIndex + buildToolsHeaderEnd.length());
+      return sketch.getPrimaryFile().getProgram().replace(header, "");
     }
+    return sketch.getPrimaryFile().getProgram();
   }
 
   /**
@@ -233,16 +231,17 @@ private boolean isBuildSettingsEqual(LinkedHashMap<String,String> first, LinkedH
     return first.keySet().containsAll(second.keySet()) && first.values().containsAll(second.values());
   }
 
-  public void setBuildSettings(EditorTab tab, LinkedHashMap<String, String> buildSettings){
-    if(tab.getSketch().getSketch() != this){
-      return;
+  public String setBuildSettings(Sketch sketch, LinkedHashMap<String, String> buildSettings){
+    if(sketch != this){
+      return "";
     }
 
     String customBoardSettingsHeader = buildSettings.entrySet().stream().map(entry-> String.format(" * %s: %s\n", entry.getKey(), entry.getValue())).collect(Collectors.joining("", buildToolsHeader, "*/"));
-    if(!isBuildSettingsEqual(getBuildSettingsFromProgram(tab.getText()),buildSettings)){
-      removeBuildSettingsHeader(tab);
-      tab.setText(customBoardSettingsHeader + ((tab.getText().charAt(0) == '\n') ? "" : "\n") + tab.getText());
+    if(!isBuildSettingsEqual(getBuildSettingsFromProgram(sketch.getPrimaryFile().getProgram()),buildSettings)){
+      String headerLessProgram = removeBuildSettingsHeader(sketch);
+      return customBoardSettingsHeader + ((headerLessProgram.charAt(0) == '\n') ? "" : "\n") + headerLessProgram;
     }
+    return "";
   }
 
   public int getCodeCount() {