processing.app.Base.java Source code

Java tutorial

Introduction

Here is the source code for processing.app.Base.java

Source

/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org
    
  Copyright (c) 2004-10 Ben Fry and Casey Reas
  Copyright (c) 2001-04 Massachusetts Institute of Technology
    
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2
  as published by the Free Software Foundation.
    
  This program 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 this program; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package processing.app;

import cc.arduino.Compiler;
import cc.arduino.Constants;
import cc.arduino.UpdatableBoardsLibsFakeURLsHandler;
import cc.arduino.UploaderUtils;
import cc.arduino.contributions.*;
import cc.arduino.contributions.libraries.*;
import cc.arduino.contributions.libraries.ui.LibraryManagerUI;
import cc.arduino.contributions.packages.ContributedPlatform;
import cc.arduino.contributions.packages.ContributionInstaller;
import cc.arduino.contributions.packages.ContributionsIndexer;
import cc.arduino.contributions.packages.ui.ContributionManagerUI;
import cc.arduino.files.DeleteFilesOnShutdown;
import cc.arduino.packages.DiscoveryManager;
import cc.arduino.view.Event;
import cc.arduino.view.JMenuUtils;
import cc.arduino.view.SplashScreenHelper;

import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;
import processing.app.debug.TargetBoard;
import processing.app.debug.TargetPackage;
import processing.app.debug.TargetPlatform;
import processing.app.helpers.*;
import processing.app.helpers.filefilters.OnlyDirs;
import processing.app.helpers.filefilters.OnlyFilesWithExtension;
import processing.app.javax.swing.filechooser.FileNameExtensionFilter;
import processing.app.legacy.PApplet;
import processing.app.macosx.ThinkDifferent;
import processing.app.packages.LibraryList;
import processing.app.packages.UserLibrary;
import processing.app.packages.UserLibraryFolder.Location;
import processing.app.syntax.PdeKeywords;
import processing.app.syntax.SketchTextAreaDefaultInputMap;
import processing.app.tools.MenuScroller;
import processing.app.tools.ZipDeflater;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.Timer;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static processing.app.I18n.tr;

/**
 * The base class for the main processing application.
 * Primary role of this class is for platform identification and
 * general interaction with the system (launching URLs, loading
 * files and images, etc) that comes from that.
 */
public class Base {

    private static final int RECENT_SKETCHES_MAX_SIZE = 10;

    private static boolean commandLine;
    public static volatile Base INSTANCE;

    public static Map<String, Object> FIND_DIALOG_STATE = new HashMap<>();
    private final ContributionInstaller contributionInstaller;
    private final LibraryInstaller libraryInstaller;
    private ContributionsSelfCheck contributionsSelfCheck;

    // set to true after the first time the menu is built.
    // so that the errors while building don't show up again.
    boolean builtOnce;

    // classpath for all known libraries for p5
    // (both those in the p5/libs folder and those with lib subfolders
    // found in the sketchbook)
    static public String librariesClassPath;

    // Location for untitled items
    static File untitledFolder;

    // p5 icon for the window
    //  static Image icon;

    //  int editorCount;
    List<Editor> editors = Collections.synchronizedList(new ArrayList<Editor>());
    Editor activeEditor;

    // these menus are shared so that the board and serial port selections
    // are the same for all windows (since the board and serial port that are
    // actually used are determined by the preferences, which are shared)
    private List<JMenu> boardsCustomMenus;
    private List<JMenuItem> programmerMenus;

    private PdeKeywords pdeKeywords;
    private final List<JMenuItem> recentSketchesMenuItems = new LinkedList<>();

    static public void main(String args[]) throws Exception {
        if (!OSUtils.isWindows()) {
            // Those properties helps enabling anti-aliasing on Linux
            // (but not on Windows where they made things worse actually
            // and the font rendering becomes ugly).

            // Those properties must be set before initializing any
            // graphic object, otherwise they don't have any effect.
            System.setProperty("awt.useSystemAAFontSettings", "on");
            System.setProperty("swing.aatext", "true");
        }
        System.setProperty("java.net.useSystemProxies", "true");

        if (OSUtils.isMacOS()) {
            System.setProperty("apple.laf.useScreenMenuBar",
                    String.valueOf(!System.getProperty("os.version").startsWith("10.13")
                            || com.apple.eawt.Application.getApplication().isAboutMenuItemPresent()));

            ThinkDifferent.init();
        }

        try {
            INSTANCE = new Base(args);
        } catch (Throwable e) {
            e.printStackTrace(System.err);
            System.exit(255);
        }
    }

    static public void initLogger() {
        Handler consoleHandler = new ConsoleLogger();
        consoleHandler.setLevel(Level.ALL);
        consoleHandler.setFormatter(new LogFormatter("%1$tl:%1$tM:%1$tS [%4$7s] %2$s: %5$s%n"));

        Logger globalLogger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
        globalLogger.setLevel(consoleHandler.getLevel());

        // Remove default
        Handler[] handlers = globalLogger.getHandlers();
        for (Handler handler : handlers) {
            globalLogger.removeHandler(handler);
        }
        Logger root = Logger.getLogger("");
        handlers = root.getHandlers();
        for (Handler handler : handlers) {
            root.removeHandler(handler);
        }

        globalLogger.addHandler(consoleHandler);

        Logger.getLogger("cc.arduino.packages.autocomplete").setParent(globalLogger);
        Logger.getLogger("br.com.criativasoft.cpluslibparser").setParent(globalLogger);
        Logger.getLogger(Base.class.getPackage().getName()).setParent(globalLogger);

    }

    static protected boolean isCommandLine() {
        return commandLine;
    }

    // Returns a File object for the given pathname. If the pathname
    // is not absolute, it is interpreted relative to the current
    // directory when starting the IDE (which is not the same as the
    // current working directory!).
    static public File absoluteFile(String path) {
        return BaseNoGui.absoluteFile(path);
    }

    public Base(String[] args) throws Exception {
        Thread deleteFilesOnShutdownThread = new Thread(DeleteFilesOnShutdown.INSTANCE);
        deleteFilesOnShutdownThread.setName("DeleteFilesOnShutdown");
        Runtime.getRuntime().addShutdownHook(deleteFilesOnShutdownThread);

        BaseNoGui.initLogger();

        initLogger();

        BaseNoGui.initPlatform();

        BaseNoGui.getPlatform().init();

        BaseNoGui.initPortableFolder();

        // Look for a possible "--preferences-file" parameter and load preferences
        BaseNoGui.initParameters(args);

        CommandlineParser parser = new CommandlineParser(args);
        parser.parseArgumentsPhase1();
        commandLine = !parser.isGuiMode();

        BaseNoGui.checkInstallationFolder();

        // If no path is set, get the default sketchbook folder for this platform
        if (BaseNoGui.getSketchbookPath() == null) {
            File defaultFolder = getDefaultSketchbookFolderOrPromptForIt();
            if (BaseNoGui.getPortableFolder() != null)
                PreferencesData.set("sketchbook.path", BaseNoGui.getPortableSketchbookFolder());
            else
                PreferencesData.set("sketchbook.path", defaultFolder.getAbsolutePath());
            if (!defaultFolder.exists()) {
                defaultFolder.mkdirs();
            }
        }

        SplashScreenHelper splash;
        if (parser.isGuiMode()) {
            // Setup all notification widgets
            splash = new SplashScreenHelper(SplashScreen.getSplashScreen());
            BaseNoGui.notifier = new GUIUserNotifier(this);

            // Setup the theme coloring fun
            Theme.init();
            System.setProperty("swing.aatext", PreferencesData.get("editor.antialias", "true"));

            // Set the look and feel before opening the window
            try {
                BaseNoGui.getPlatform().setLookAndFeel();
            } catch (Exception e) {
                // ignore
            }

            // Use native popups so they don't look so crappy on osx
            JPopupMenu.setDefaultLightWeightPopupEnabled(false);
        } else {
            splash = new SplashScreenHelper(null);
        }

        splash.splashText(tr("Loading configuration..."));

        BaseNoGui.initVersion();

        // Don't put anything above this line that might make GUI,
        // because the platform has to be inited properly first.

        // Create a location for untitled sketches
        untitledFolder = FileUtils.createTempFolder("untitled" + new Random().nextInt(Integer.MAX_VALUE), ".tmp");
        DeleteFilesOnShutdown.add(untitledFolder);

        splash.splashText(tr("Initializing packages..."));
        BaseNoGui.initPackages();

        splash.splashText(tr("Preparing boards..."));

        if (!isCommandLine()) {
            rebuildBoardsMenu();
            rebuildProgrammerMenu();
        } else {
            TargetBoard lastSelectedBoard = BaseNoGui.getTargetBoard();
            if (lastSelectedBoard != null)
                BaseNoGui.selectBoard(lastSelectedBoard);
        }

        // Setup board-dependent variables.
        onBoardOrPortChange();

        pdeKeywords = new PdeKeywords();
        pdeKeywords.reload();

        contributionInstaller = new ContributionInstaller(BaseNoGui.getPlatform(),
                new GPGDetachedSignatureVerifier());
        libraryInstaller = new LibraryInstaller(BaseNoGui.getPlatform());

        parser.parseArgumentsPhase2();

        // Save the preferences. For GUI mode, this happens in the quit
        // handler, but for other modes we should also make sure to save
        // them.
        if (parser.isForceSavePrefs()) {
            PreferencesData.save();
        }

        if (parser.isInstallBoard()) {
            ContributionsIndexer indexer = new ContributionsIndexer(BaseNoGui.getSettingsFolder(),
                    BaseNoGui.getHardwareFolder(), BaseNoGui.getPlatform(), new GPGDetachedSignatureVerifier());
            ProgressListener progressListener = new ConsoleProgressListener();

            List<String> downloadedPackageIndexFiles = contributionInstaller.updateIndex(progressListener);
            contributionInstaller.deleteUnknownFiles(downloadedPackageIndexFiles);
            indexer.parseIndex();
            indexer.syncWithFilesystem();

            String[] boardToInstallParts = parser.getBoardToInstall().split(":");

            ContributedPlatform selected = null;
            if (boardToInstallParts.length == 3) {
                selected = indexer.getIndex().findPlatform(boardToInstallParts[0], boardToInstallParts[1],
                        VersionHelper.valueOf(boardToInstallParts[2]).toString());
            } else if (boardToInstallParts.length == 2) {
                List<ContributedPlatform> platformsByName = indexer.getIndex().findPlatforms(boardToInstallParts[0],
                        boardToInstallParts[1]);
                Collections.sort(platformsByName, new DownloadableContributionVersionComparator());
                if (!platformsByName.isEmpty()) {
                    selected = platformsByName.get(platformsByName.size() - 1);
                }
            }
            if (selected == null) {
                System.out.println(tr("Selected board is not available"));
                System.exit(1);
            }

            ContributedPlatform installed = indexer.getInstalled(boardToInstallParts[0], boardToInstallParts[1]);

            if (!selected.isBuiltIn()) {
                contributionInstaller.install(selected, progressListener);
            }

            if (installed != null && !installed.isBuiltIn()) {
                contributionInstaller.remove(installed);
            }

            System.exit(0);

        } else if (parser.isInstallLibrary()) {
            BaseNoGui.onBoardOrPortChange();

            ProgressListener progressListener = new ConsoleProgressListener();
            libraryInstaller.updateIndex(progressListener);

            LibrariesIndexer indexer = new LibrariesIndexer(BaseNoGui.getSettingsFolder());
            indexer.parseIndex();
            indexer.setLibrariesFolders(BaseNoGui.getLibrariesFolders());
            indexer.rescanLibraries();

            for (String library : parser.getLibraryToInstall().split(",")) {
                String[] libraryToInstallParts = library.split(":");

                ContributedLibrary selected = null;
                if (libraryToInstallParts.length == 2) {
                    selected = indexer.getIndex().find(libraryToInstallParts[0],
                            VersionHelper.valueOf(libraryToInstallParts[1]).toString());
                } else if (libraryToInstallParts.length == 1) {
                    List<ContributedLibrary> librariesByName = indexer.getIndex().find(libraryToInstallParts[0]);
                    Collections.sort(librariesByName, new DownloadableContributionVersionComparator());
                    if (!librariesByName.isEmpty()) {
                        selected = librariesByName.get(librariesByName.size() - 1);
                    }
                }
                if (selected == null) {
                    System.out.println(tr("Selected library is not available"));
                    System.exit(1);
                }

                Optional<ContributedLibrary> mayInstalled = indexer.getIndex()
                        .getInstalled(libraryToInstallParts[0]);
                if (mayInstalled.isPresent() && selected.isIDEBuiltIn()) {
                    System.out.println(tr(I18n.format(
                            "Library {0} is available as built-in in the IDE.\nRemoving the other version {1} installed in the sketchbook...",
                            library, mayInstalled.get().getParsedVersion())));
                    libraryInstaller.remove(mayInstalled.get(), progressListener);
                } else {
                    libraryInstaller.install(selected, mayInstalled, progressListener);
                }
            }

            System.exit(0);

        } else if (parser.isVerifyOrUploadMode()) {
            // Set verbosity for command line build
            PreferencesData.setBoolean("build.verbose", parser.isDoVerboseBuild());
            PreferencesData.setBoolean("upload.verbose", parser.isDoVerboseUpload());

            // Set preserve-temp flag
            PreferencesData.setBoolean("runtime.preserve.temp.files", parser.isPreserveTempFiles());

            // Make sure these verbosity preferences are only for the current session
            PreferencesData.setDoSave(false);

            Sketch sketch = null;
            String outputFile = null;

            try {
                // Build
                splash.splashText(tr("Verifying..."));

                File sketchFile = BaseNoGui.absoluteFile(parser.getFilenames().get(0));
                sketch = new Sketch(sketchFile);

                outputFile = new Compiler(sketch).build(progress -> {
                }, false);
            } catch (Exception e) {
                // Error during build
                e.printStackTrace();
                System.exit(1);
            }

            if (parser.isUploadMode()) {
                // Upload
                splash.splashText(tr("Uploading..."));

                try {
                    List<String> warnings = new ArrayList<>();
                    UploaderUtils uploader = new UploaderUtils();
                    boolean res = uploader.upload(sketch, null, outputFile, parser.isDoUseProgrammer(),
                            parser.isNoUploadPort(), warnings);
                    for (String warning : warnings) {
                        System.out.println(tr("Warning") + ": " + warning);
                    }
                    if (!res) {
                        throw new Exception();
                    }
                } catch (Exception e) {
                    // Error during upload
                    System.out.flush();
                    System.err.flush();
                    System.err.println(tr("An error occurred while uploading the sketch"));
                    System.exit(1);
                }
            }

            // No errors exit gracefully
            System.exit(0);
        } else if (parser.isGuiMode()) {
            splash.splashText(tr("Starting..."));

            for (String path : parser.getFilenames()) {
                // Correctly resolve relative paths
                File file = absoluteFile(path);

                // Fix a problem with systems that use a non-ASCII languages. Paths are
                // being passed in with 8.3 syntax, which makes the sketch loader code
                // unhappy, since the sketch folder naming doesn't match up correctly.
                // http://dev.processing.org/bugs/show_bug.cgi?id=1089
                if (OSUtils.isWindows()) {
                    try {
                        file = file.getCanonicalFile();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                if (!parser.isForceSavePrefs())
                    PreferencesData.setDoSave(true);
                if (handleOpen(file, retrieveSketchLocation(".default"), false) == null) {
                    String mess = I18n.format(tr("Failed to open sketch: \"{0}\""), path);
                    // Open failure is fatal in upload/verify mode
                    if (parser.isVerifyOrUploadMode())
                        showError(null, mess, 2);
                    else
                        showWarning(null, mess, null);
                }
            }

            installKeyboardInputMap();

            // Check if there were previously opened sketches to be restored
            restoreSketches();

            // Create a new empty window (will be replaced with any files to be opened)
            if (editors.isEmpty()) {
                handleNew();
            }

            new Thread(new BuiltInCoreIsNewerCheck(this)).start();

            // Check for boards which need an additional core
            new Thread(new NewBoardListener(this)).start();

            // Check for updates
            if (PreferencesData.getBoolean("update.check")) {
                new UpdateCheck(this);

                contributionsSelfCheck = new ContributionsSelfCheck(this,
                        new UpdatableBoardsLibsFakeURLsHandler(this), contributionInstaller, libraryInstaller);
                new Timer(false).schedule(contributionsSelfCheck,
                        Constants.BOARDS_LIBS_UPDATABLE_CHECK_START_PERIOD);
            }

        } else if (parser.isNoOpMode()) {
            // Do nothing (intended for only changing preferences)
            System.exit(0);
        } else if (parser.isGetPrefMode()) {
            BaseNoGui.dumpPrefs(parser);
        } else if (parser.isVersionMode()) {
            System.out.println("Arduino: " + BaseNoGui.VERSION_NAME_LONG);
            System.exit(0);
        }
    }

    private void installKeyboardInputMap() {
        UIManager.put("RSyntaxTextAreaUI.inputMap", new SketchTextAreaDefaultInputMap());
    }

    /**
     * Post-constructor setup for the editor area. Loads the last
     * sketch that was used (if any), and restores other Editor settings.
     * The complement to "storePreferences", this is called when the
     * application is first launched.
     *
     * @throws Exception
     */
    protected boolean restoreSketches() throws Exception {
        // Iterate through all sketches that were open last time p5 was running.
        // If !windowPositionValid, then ignore the coordinates found for each.

        // Save the sketch path and window placement for each open sketch
        int count = PreferencesData.getInteger("last.sketch.count");
        int opened = 0;
        for (int i = count - 1; i >= 0; i--) {
            String path = PreferencesData.get("last.sketch" + i + ".path");
            if (path == null) {
                continue;
            }
            if (BaseNoGui.getPortableFolder() != null && !new File(path).isAbsolute()) {
                File absolute = new File(BaseNoGui.getPortableFolder(), path);
                try {
                    path = absolute.getCanonicalPath();
                } catch (IOException e) {
                    // path unchanged.
                }
            }
            int[] location = retrieveSketchLocation("" + i);
            // If file did not exist, null will be returned for the Editor
            if (handleOpen(new File(path), location, nextEditorLocation(), false, false) != null) {
                opened++;
            }
        }
        return (opened > 0);
    }

    /**
     * Store screen dimensions on last close
     */
    protected void storeScreenDimensions() {
        // Save the width and height of the screen
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        PreferencesData.setInteger("last.screen.width", screen.width);
        PreferencesData.setInteger("last.screen.height", screen.height);
    }

    /**
     * Store list of sketches that are currently open.
     * Called when the application is quitting and documents are still open.
     */
    protected void storeSketches() {

        // If there is only one sketch opened save his position as default
        if (editors.size() == 1) {
            storeSketchLocation(editors.get(0), ".default");
        }

        // Save the sketch path and window placement for each open sketch
        String untitledPath = untitledFolder.getAbsolutePath();
        List<Editor> reversedEditors = new LinkedList<>(editors);
        Collections.reverse(reversedEditors);
        int index = 0;
        for (Editor editor : reversedEditors) {
            Sketch sketch = editor.getSketch();
            String path = sketch.getMainFilePath();
            // Skip untitled sketches if they do not contains changes.
            if (path.startsWith(untitledPath) && !sketch.isModified()) {
                continue;
            }
            storeSketchLocation(editor, "" + index);
            index++;
        }
        PreferencesData.setInteger("last.sketch.count", index);
    }

    private void storeSketchLocation(Editor editor, String index) {
        String path = editor.getSketch().getMainFilePath();
        String loc = StringUtils.join(editor.getPlacement(), ',');
        PreferencesData.set("last.sketch" + index + ".path", path);
        PreferencesData.set("last.sketch" + index + ".location", loc);
    }

    private int[] retrieveSketchLocation(String index) {
        if (PreferencesData.get("last.screen.height") == null)
            return defaultEditorLocation();

        // if screen size has changed, the window coordinates no longer
        // make sense, so don't use them unless they're identical
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        int screenW = PreferencesData.getInteger("last.screen.width");
        int screenH = PreferencesData.getInteger("last.screen.height");

        if ((screen.width != screenW) || (screen.height != screenH))
            return defaultEditorLocation();

        String locationStr = PreferencesData.get("last.sketch" + index + ".location");
        if (locationStr == null)
            return defaultEditorLocation();
        return PApplet.parseInt(PApplet.split(locationStr, ','));
    }

    protected void storeRecentSketches(SketchController sketch) {
        if (sketch.isUntitled()) {
            return;
        }

        Set<String> sketches = new LinkedHashSet<>();
        sketches.add(sketch.getSketch().getMainFilePath());
        sketches.addAll(PreferencesData.getCollection("recent.sketches"));

        PreferencesData.setCollection("recent.sketches", sketches);
    }

    protected void removeRecentSketchPath(String path) {
        Collection<String> sketches = new LinkedList<>(PreferencesData.getCollection("recent.sketches"));
        sketches.remove(path);
        PreferencesData.setCollection("recent.sketches", sketches);
    }

    // Because of variations in native windowing systems, no guarantees about
    // changes to the focused and active Windows can be made. Developers must
    // never assume that this Window is the focused or active Window until this
    // Window receives a WINDOW_GAINED_FOCUS or WINDOW_ACTIVATED event.
    protected void handleActivated(Editor whichEditor) {
        activeEditor = whichEditor;
        activeEditor.rebuildRecentSketchesMenu();
        if (PreferencesData.getBoolean("editor.external")) {
            try {
                // If the list of files on disk changed, recreate the tabs for them
                if (activeEditor.getSketch().reload())
                    activeEditor.createTabs();
                else // Let the current tab know it was activated, so it can reload
                    activeEditor.getCurrentTab().activated();
            } catch (IOException e) {
                System.err.println(e);
            }
        }
    }

    protected int[] defaultEditorLocation() {
        int defaultWidth = PreferencesData.getInteger("editor.window.width.default");
        int defaultHeight = PreferencesData.getInteger("editor.window.height.default");
        Rectangle screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()
                .getDefaultConfiguration().getBounds();
        return new int[] { (screen.width - defaultWidth) / 2, (screen.height - defaultHeight) / 2, defaultWidth,
                defaultHeight, 0 };
    }

    protected int[] nextEditorLocation() {
        if (activeEditor == null) {
            // If no current active editor, use default placement
            return defaultEditorLocation();
        }

        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();

        // With a currently active editor, open the new window
        // using the same dimensions, but offset slightly.
        synchronized (editors) {
            int[] location = activeEditor.getPlacement();

            // Just in case the bounds for that window are bad
            final int OVER = 50;
            location[0] += OVER;
            location[1] += OVER;

            if (location[0] == OVER || location[2] == OVER || location[0] + location[2] > screen.width
                    || location[1] + location[3] > screen.height) {
                // Warp the next window to a randomish location on screen.
                int[] l = defaultEditorLocation();
                l[0] *= Math.random() * 2;
                l[1] *= Math.random() * 2;
                return l;
            }

            return location;
        }
    }

    // .................................................................

    boolean breakTime = false;
    String[] months = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" };

    protected File createNewUntitled() throws IOException {
        File newbieDir = null;
        String newbieName = null;

        // In 0126, untitled sketches will begin in the temp folder,
        // and then moved to a new location because Save will default to Save As.
        File sketchbookDir = BaseNoGui.getSketchbookFolder();
        File newbieParentDir = untitledFolder;

        // Use a generic name like sketch_031008a, the date plus a char
        int index = 0;
        //SimpleDateFormat formatter = new SimpleDateFormat("yyMMdd");
        //SimpleDateFormat formatter = new SimpleDateFormat("MMMdd");
        //String purty = formatter.format(new Date()).toLowerCase();
        Calendar cal = Calendar.getInstance();
        int day = cal.get(Calendar.DAY_OF_MONTH); // 1..31
        int month = cal.get(Calendar.MONTH); // 0..11
        String purty = months[month] + PApplet.nf(day, 2);

        do {
            if (index == 26 * 26) {
                // In 0166, avoid running past zz by sending people outdoors.
                if (!breakTime) {
                    showWarning(tr("Time for a Break"),
                            tr("You've reached the limit for auto naming of new sketches\n"
                                    + "for the day. How about going for a walk instead?"),
                            null);
                    breakTime = true;
                } else {
                    showWarning(tr("Sunshine"), tr("No really, time for some fresh air for you."), null);
                }
                return null;
            }

            int multiples = index / 26;

            if (multiples > 0) {
                newbieName = ((char) ('a' + (multiples - 1))) + "" + ((char) ('a' + (index % 26))) + "";
            } else {
                newbieName = ((char) ('a' + index)) + "";
            }
            newbieName = "sketch_" + purty + newbieName;
            newbieDir = new File(newbieParentDir, newbieName);
            index++;
            // Make sure it's not in the temp folder *and* it's not in the sketchbook
        } while (newbieDir.exists() || new File(sketchbookDir, newbieName).exists());

        // Make the directory for the new sketch
        newbieDir.mkdirs();

        // Make an empty pde file
        File newbieFile = new File(newbieDir, newbieName + ".ino");
        if (!newbieFile.createNewFile()) {
            throw new IOException();
        }
        FileUtils.copyFile(
                new File(getContentFile("examples"),
                        "01.Basics" + File.separator + "BareMinimum" + File.separator + "BareMinimum.ino"),
                newbieFile);
        return newbieFile;
    }

    /**
     * Create a new untitled document in a new sketch window.
     *
     * @throws Exception
     */
    public void handleNew() throws Exception {
        try {
            File file = createNewUntitled();
            if (file != null) {
                handleOpen(file, true);
            }

        } catch (IOException e) {
            if (activeEditor != null) {
                activeEditor.statusError(e);
            }
        }
    }

    /**
     * Prompt for a sketch to open, and open it in a new window.
     *
     * @throws Exception
     */
    public void handleOpenPrompt() throws Exception {
        // get the frontmost window frame for placing file dialog
        FileDialog fd = new FileDialog(activeEditor, tr("Open an Arduino sketch..."), FileDialog.LOAD);
        File lastFolder = new File(
                PreferencesData.get("last.folder", BaseNoGui.getSketchbookFolder().getAbsolutePath()));
        if (lastFolder.exists() && lastFolder.isFile()) {
            lastFolder = lastFolder.getParentFile();
        }
        fd.setDirectory(lastFolder.getAbsolutePath());

        // Only show .pde files as eligible bachelors
        fd.setFilenameFilter(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.toLowerCase().endsWith(".ino") || name.toLowerCase().endsWith(".pde");
            }
        });

        fd.setVisible(true);

        String directory = fd.getDirectory();
        String filename = fd.getFile();

        // User canceled selection
        if (filename == null)
            return;

        File inputFile = new File(directory, filename);

        PreferencesData.set("last.folder", inputFile.getAbsolutePath());
        handleOpen(inputFile);
    }

    /**
     * Open a sketch in a new window.
     *
     * @param file File to open
     * @return the Editor object, so that properties (like 'untitled')
     * can be set by the caller
     * @throws Exception
     */
    public Editor handleOpen(File file) throws Exception {
        return handleOpen(file, false);
    }

    public Editor handleOpen(File file, boolean untitled) throws Exception {
        return handleOpen(file, nextEditorLocation(), untitled);
    }

    protected Editor handleOpen(File file, int[] location, boolean untitled) throws Exception {
        return handleOpen(file, location, location, true, untitled);
    }

    protected Editor handleOpen(File file, int[] storedLocation, int[] defaultLocation, boolean storeOpenedSketches,
            boolean untitled) throws Exception {
        if (!file.exists())
            return null;

        // Cycle through open windows to make sure that it's not already open.
        for (Editor editor : editors) {
            if (editor.getSketch().getPrimaryFile().getFile().equals(file)) {
                editor.toFront();
                return editor;
            }
        }

        Editor editor = new Editor(this, file, storedLocation, defaultLocation, BaseNoGui.getPlatform());

        // Make sure that the sketch actually loaded
        if (editor.getSketchController() == null) {
            return null; // Just walk away quietly
        }

        editor.untitled = untitled;

        editors.add(editor);

        if (storeOpenedSketches) {
            // Store information on who's open and running
            // (in case there's a crash or something that can't be recovered)
            storeSketches();
            storeRecentSketches(editor.getSketchController());
            rebuildRecentSketchesMenuItems();
            PreferencesData.save();
        }

        // now that we're ready, show the window
        // (don't do earlier, cuz we might move it based on a window being closed)
        SwingUtilities.invokeLater(() -> editor.setVisible(true));

        return editor;
    }

    protected void rebuildRecentSketchesMenuItems() {
        Set<File> recentSketches = new LinkedHashSet<File>() {

            @Override
            public boolean add(File file) {
                if (size() >= RECENT_SKETCHES_MAX_SIZE) {
                    return false;
                }
                return super.add(file);
            }
        };

        for (String path : PreferencesData.getCollection("recent.sketches")) {
            File file = new File(path);
            if (file.exists()) {
                recentSketches.add(file);
            }
        }

        recentSketchesMenuItems.clear();
        for (final File recentSketch : recentSketches) {
            JMenuItem recentSketchMenuItem = new JMenuItem(recentSketch.getParentFile().getName());
            recentSketchMenuItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent actionEvent) {
                    try {
                        handleOpen(recentSketch);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            recentSketchesMenuItems.add(recentSketchMenuItem);
        }
    }

    /**
     * Close a sketch as specified by its editor window.
     *
     * @param editor Editor object of the sketch to be closed.
     * @return true if succeeded in closing, false if canceled.
     */
    public boolean handleClose(Editor editor) {
        // Check if modified
        //    boolean immediate = editors.size() == 1;
        if (!editor.checkModified()) {
            return false;
        }

        if (editors.size() == 1) {
            storeScreenDimensions();
            storeSketches();

            // This will store the sketch count as zero
            editors.remove(editor);
            try {
                Editor.serialMonitor.close();
            } catch (Exception e) {
                //ignore
            }
            rebuildRecentSketchesMenuItems();

            // Save out the current prefs state
            PreferencesData.save();

            // Since this wasn't an actual Quit event, call System.exit()
            System.exit(0);

        } else {
            // More than one editor window open,
            // proceed with closing the current window.
            editor.setVisible(false);
            editor.dispose();
            //      for (int i = 0; i < editorCount; i++) {
            //        if (editor == editors[i]) {
            //          for (int j = i; j < editorCount-1; j++) {
            //            editors[j] = editors[j+1];
            //          }
            //          editorCount--;
            //          // Set to null so that garbage collection occurs
            //          editors[editorCount] = null;
            //        }
            //      }
            editors.remove(editor);
        }
        return true;
    }

    /**
     * Handler for File &rarr; Quit.
     *
     * @return false if canceled, true otherwise.
     */
    public boolean handleQuit() {
        // If quit is canceled, this will be replaced anyway
        // by a later handleQuit() that is not canceled.
        storeScreenDimensions();
        storeSketches();
        try {
            Editor.serialMonitor.close();
        } catch (Exception e) {
            // ignore
        }

        if (handleQuitEach()) {
            // Save out the current prefs state
            PreferencesData.save();

            if (!OSUtils.hasMacOSStyleMenus()) {
                // If this was fired from the menu or an AppleEvent (the Finder),
                // then Mac OS X will send the terminate signal itself.
                System.exit(0);
            }
            return true;
        }
        return false;
    }

    /**
     * Attempt to close each open sketch in preparation for quitting.
     *
     * @return false if canceled along the way
     */
    protected boolean handleQuitEach() {
        for (Editor editor : editors) {
            if (!editor.checkModified()) {
                return false;
            }
        }
        return true;
    }

    // .................................................................

    /**
     * Asynchronous version of menu rebuild to be used on save and rename
     * to prevent the interface from locking up until the menus are done.
     */
    public void rebuildSketchbookMenus() {
        //System.out.println("async enter");
        //new Exception().printStackTrace();
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                //System.out.println("starting rebuild");
                rebuildSketchbookMenu(Editor.sketchbookMenu);
                rebuildToolbarMenu(Editor.toolbarMenu);
                //System.out.println("done with rebuild");
            }
        });
        //System.out.println("async exit");
    }

    protected void rebuildToolbarMenu(JMenu menu) {
        JMenuItem item;
        menu.removeAll();

        // Add the single "Open" item
        item = Editor.newJMenuItem(tr("Open..."), 'O');
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {
                    handleOpenPrompt();
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
        });
        menu.add(item);
        menu.addSeparator();

        // Add a list of all sketches and subfolders
        boolean sketches = addSketches(menu, BaseNoGui.getSketchbookFolder());
        if (sketches)
            menu.addSeparator();

        // Add each of the subfolders of examples directly to the menu
        boolean found = addSketches(menu, BaseNoGui.getExamplesFolder());
        if (found)
            menu.addSeparator();
    }

    protected void rebuildSketchbookMenu(JMenu menu) {
        menu.removeAll();
        addSketches(menu, BaseNoGui.getSketchbookFolder());

        JMenu librariesMenu = JMenuUtils.findSubMenuWithLabel(menu, "libraries");
        if (librariesMenu != null) {
            menu.remove(librariesMenu);
        }
        JMenu hardwareMenu = JMenuUtils.findSubMenuWithLabel(menu, "hardware");
        if (hardwareMenu != null) {
            menu.remove(hardwareMenu);
        }
    }

    private LibraryList getSortedLibraries() {
        LibraryList installedLibraries = BaseNoGui.librariesIndexer.getInstalledLibraries();
        Collections.sort(installedLibraries, new LibraryOfSameTypeComparator());
        return installedLibraries;
    }

    public void rebuildImportMenu(JMenu importMenu) {
        if (importMenu == null)
            return;
        importMenu.removeAll();

        JMenuItem menu = new JMenuItem(tr("Manage Libraries..."));
        // Ctrl+Shift+I on Windows and Linux, Command+Shift+I on macOS
        menu.setAccelerator(KeyStroke.getKeyStroke('I',
                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | ActionEvent.SHIFT_MASK));
        menu.addActionListener(e -> openLibraryManager("", ""));
        importMenu.add(menu);
        importMenu.addSeparator();

        JMenuItem addLibraryMenuItem = new JMenuItem(tr("Add .ZIP Library..."));
        addLibraryMenuItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Base.this.handleAddLibrary();
                BaseNoGui.librariesIndexer.rescanLibraries();
                Base.this.onBoardOrPortChange();
                Base.this.rebuildImportMenu(Editor.importMenu);
                Base.this.rebuildExamplesMenu(Editor.examplesMenu);
            }
        });
        importMenu.add(addLibraryMenuItem);
        importMenu.addSeparator();

        // Split between user supplied libraries and IDE libraries
        TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform();

        if (targetPlatform != null) {
            LibraryList libs = getSortedLibraries();
            String lastLibType = null;
            for (UserLibrary lib : libs) {
                String libType = lib.getTypes().get(0);
                if (!libType.equals(lastLibType)) {
                    if (lastLibType != null) {
                        importMenu.addSeparator();
                    }
                    lastLibType = libType;
                    JMenuItem platformItem = new JMenuItem(I18n.format(tr("{0} libraries"), tr(lastLibType)));
                    platformItem.setEnabled(false);
                    importMenu.add(platformItem);
                }

                AbstractAction action = new AbstractAction(lib.getName()) {
                    public void actionPerformed(ActionEvent event) {
                        UserLibrary l = (UserLibrary) getValue("library");
                        try {
                            activeEditor.getSketchController().importLibrary(l);
                        } catch (IOException e) {
                            showWarning(tr("Error"),
                                    I18n.format("Unable to list header files in {0}", l.getSrcFolder()), e);
                        }
                    }
                };
                action.putValue("library", lib);

                // Add new element at the bottom
                JMenuItem item = new JMenuItem(action);
                item.putClientProperty("library", lib);
                importMenu.add(item);
            }
        }
    }

    public void rebuildExamplesMenu(JMenu menu) {
        if (menu == null) {
            return;
        }

        menu.removeAll();

        // Add examples from distribution "example" folder
        JMenuItem label = new JMenuItem(tr("Built-in Examples"));
        label.setEnabled(false);
        menu.add(label);
        boolean found = addSketches(menu, BaseNoGui.getExamplesFolder());
        if (found) {
            menu.addSeparator();
        }

        // Libraries can come from 4 locations: collect info about all four
        String boardId = null;
        String referencedPlatformName = null;
        String myArch = null;
        TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform();
        if (targetPlatform != null) {
            myArch = targetPlatform.getId();
            boardId = BaseNoGui.getTargetBoard().getName();
            String core = BaseNoGui.getBoardPreferences().get("build.core", "arduino");
            if (core.contains(":")) {
                String refcore = core.split(":")[0];
                TargetPlatform referencedPlatform = BaseNoGui.getTargetPlatform(refcore, myArch);
                if (referencedPlatform != null) {
                    referencedPlatformName = referencedPlatform.getPreferences().get("name");
                }
            }
        }

        // Divide the libraries into 7 lists, corresponding to the 4 locations
        // with the retired IDE libs further divided into their own list, and
        // any incompatible sketchbook libs further divided into their own list.
        // The 7th list of "other" libraries should always be empty, but serves
        // as a safety feature to prevent any library from vanishing.
        LibraryList allLibraries = BaseNoGui.librariesIndexer.getInstalledLibraries();
        LibraryList ideLibs = new LibraryList();
        LibraryList retiredIdeLibs = new LibraryList();
        LibraryList platformLibs = new LibraryList();
        LibraryList referencedPlatformLibs = new LibraryList();
        LibraryList sketchbookLibs = new LibraryList();
        LibraryList sketchbookIncompatibleLibs = new LibraryList();
        LibraryList otherLibs = new LibraryList();
        for (UserLibrary lib : allLibraries) {
            // Get the library's location - used for sorting into categories
            Location location = lib.getLocation();
            // Is this library compatible?
            List<String> arch = lib.getArchitectures();
            boolean compatible;
            if (myArch == null || arch == null || arch.contains("*")) {
                compatible = true;
            } else {
                compatible = arch.contains(myArch);
            }
            // IDE Libaries (including retired)
            if (location == Location.IDE_BUILTIN) {
                if (compatible) {
                    // only compatible IDE libs are shown
                    if (lib.getTypes().contains("Retired")) {
                        retiredIdeLibs.add(lib);
                    } else {
                        ideLibs.add(lib);
                    }
                }
                // Platform Libraries
            } else if (location == Location.CORE) {
                // all platform libs are assumed to be compatible
                platformLibs.add(lib);
                // Referenced Platform Libraries
            } else if (location == Location.REFERENCED_CORE) {
                // all referenced platform libs are assumed to be compatible
                referencedPlatformLibs.add(lib);
                // Sketchbook Libraries (including incompatible)
            } else if (location == Location.SKETCHBOOK) {
                if (compatible) {
                    // libraries promoted from sketchbook (behave as builtin)
                    if (!lib.getTypes().isEmpty() && lib.getTypes().contains("Arduino")
                            && lib.getArchitectures().contains("*")) {
                        ideLibs.add(lib);
                    } else {
                        sketchbookLibs.add(lib);
                    }
                } else {
                    sketchbookIncompatibleLibs.add(lib);
                }
                // Other libraries of unknown type (should never occur)
            } else {
                otherLibs.add(lib);
            }
        }

        // Add examples from libraries
        if (!ideLibs.isEmpty()) {
            ideLibs.sort();
            label = new JMenuItem(tr("Examples for any board"));
            label.setEnabled(false);
            menu.add(label);
        }
        for (UserLibrary lib : ideLibs) {
            addSketchesSubmenu(menu, lib);
        }

        if (!retiredIdeLibs.isEmpty()) {
            retiredIdeLibs.sort();
            JMenu retired = new JMenu(tr("RETIRED"));
            menu.add(retired);
            for (UserLibrary lib : retiredIdeLibs) {
                addSketchesSubmenu(retired, lib);
            }
        }

        if (!platformLibs.isEmpty()) {
            menu.addSeparator();
            platformLibs.sort();
            label = new JMenuItem(I18n.format(tr("Examples for {0}"), boardId));
            label.setEnabled(false);
            menu.add(label);
            for (UserLibrary lib : platformLibs) {
                addSketchesSubmenu(menu, lib);
            }
        }

        if (!referencedPlatformLibs.isEmpty()) {
            menu.addSeparator();
            referencedPlatformLibs.sort();
            label = new JMenuItem(I18n.format(tr("Examples for {0}"), referencedPlatformName));
            label.setEnabled(false);
            menu.add(label);
            for (UserLibrary lib : referencedPlatformLibs) {
                addSketchesSubmenu(menu, lib);
            }
        }

        if (!sketchbookLibs.isEmpty()) {
            menu.addSeparator();
            sketchbookLibs.sort();
            label = new JMenuItem(tr("Examples from Custom Libraries"));
            label.setEnabled(false);
            menu.add(label);
            for (UserLibrary lib : sketchbookLibs) {
                addSketchesSubmenu(menu, lib);
            }
        }

        if (!sketchbookIncompatibleLibs.isEmpty()) {
            sketchbookIncompatibleLibs.sort();
            JMenu incompatible = new JMenu(tr("INCOMPATIBLE"));
            menu.add(incompatible);
            for (UserLibrary lib : sketchbookIncompatibleLibs) {
                addSketchesSubmenu(incompatible, lib);
            }
        }

        if (!otherLibs.isEmpty()) {
            menu.addSeparator();
            otherLibs.sort();
            label = new JMenuItem(tr("Examples from Other Libraries"));
            label.setEnabled(false);
            menu.add(label);
            for (UserLibrary lib : otherLibs) {
                addSketchesSubmenu(menu, lib);
            }
        }
    }

    private static String priorPlatformFolder;
    private static boolean newLibraryImported;

    public void onBoardOrPortChange() {
        BaseNoGui.onBoardOrPortChange();

        // reload keywords when package/platform changes
        TargetPlatform tp = BaseNoGui.getTargetPlatform();
        if (tp != null) {
            String platformFolder = tp.getFolder().getAbsolutePath();
            if (priorPlatformFolder == null || !priorPlatformFolder.equals(platformFolder) || newLibraryImported) {
                pdeKeywords = new PdeKeywords();
                pdeKeywords.reload();
                priorPlatformFolder = platformFolder;
                newLibraryImported = false;
                for (Editor editor : editors) {
                    editor.updateKeywords(pdeKeywords);
                }
            }
        }

        // Update editors status bar
        for (Editor editor : editors) {
            editor.onBoardOrPortChange();
        }
    }

    public void openLibraryManager(final String filterText, String dropdownItem) {
        if (contributionsSelfCheck != null) {
            contributionsSelfCheck.cancel();
        }
        @SuppressWarnings("serial")
        LibraryManagerUI managerUI = new LibraryManagerUI(activeEditor, libraryInstaller) {
            @Override
            protected void onIndexesUpdated() throws Exception {
                BaseNoGui.initPackages();
                rebuildBoardsMenu();
                rebuildProgrammerMenu();
                onBoardOrPortChange();
                updateUI();
                if (StringUtils.isNotEmpty(dropdownItem)) {
                    selectDropdownItemByClassName(dropdownItem);
                }
                if (StringUtils.isNotEmpty(filterText)) {
                    setFilterText(filterText);
                }
            }
        };
        managerUI.setLocationRelativeTo(activeEditor);
        managerUI.updateUI();
        managerUI.setVisible(true);
        // Manager dialog is modal, waits here until closed

        //handleAddLibrary();
        newLibraryImported = true;
        onBoardOrPortChange();
        rebuildImportMenu(Editor.importMenu);
        rebuildExamplesMenu(Editor.examplesMenu);
    }

    public void openBoardsManager(final String filterText, String dropdownItem) throws Exception {
        if (contributionsSelfCheck != null) {
            contributionsSelfCheck.cancel();
        }
        @SuppressWarnings("serial")
        ContributionManagerUI managerUI = new ContributionManagerUI(activeEditor, contributionInstaller) {
            @Override
            protected void onIndexesUpdated() throws Exception {
                BaseNoGui.initPackages();
                rebuildBoardsMenu();
                rebuildProgrammerMenu();
                updateUI();
                if (StringUtils.isNotEmpty(dropdownItem)) {
                    selectDropdownItemByClassName(dropdownItem);
                }
                if (StringUtils.isNotEmpty(filterText)) {
                    setFilterText(filterText);
                }
            }
        };
        managerUI.setLocationRelativeTo(activeEditor);
        managerUI.updateUI();
        managerUI.setVisible(true);
        // Installer dialog is modal, waits here until closed

        // Reload all boards (that may have been installed/updated/removed)
        BaseNoGui.initPackages();
        rebuildBoardsMenu();
        rebuildProgrammerMenu();
        onBoardOrPortChange();
    }

    public void rebuildBoardsMenu() throws Exception {
        boardsCustomMenus = new LinkedList<>();

        // The first custom menu is the "Board" selection submenu
        JMenu boardMenu = new JMenu(tr("Board"));
        boardMenu.putClientProperty("removeOnWindowDeactivation", true);
        MenuScroller.setScrollerFor(boardMenu).setTopFixedCount(1);

        boardMenu.add(new JMenuItem(new AbstractAction(tr("Boards Manager...")) {
            public void actionPerformed(ActionEvent actionevent) {
                String filterText = "";
                String dropdownItem = "";
                if (actionevent instanceof Event) {
                    filterText = ((Event) actionevent).getPayload().get("filterText").toString();
                    dropdownItem = ((Event) actionevent).getPayload().get("dropdownItem").toString();
                }
                try {
                    openBoardsManager(filterText, dropdownItem);
                } catch (Exception e) {
                    //TODO show error
                    e.printStackTrace();
                }
            }
        }));
        boardsCustomMenus.add(boardMenu);

        // If there are no platforms installed we are done
        if (BaseNoGui.packages.size() == 0)
            return;

        // Separate "Install boards..." command from installed boards
        boardMenu.add(new JSeparator());

        // Generate custom menus for all platforms
        Set<String> customMenusTitles = new LinkedHashSet<>();
        for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
            for (TargetPlatform targetPlatform : targetPackage.platforms()) {
                customMenusTitles.addAll(targetPlatform.getCustomMenus().values());
            }
        }
        for (String customMenuTitle : customMenusTitles) {
            JMenu customMenu = new JMenu(tr(customMenuTitle));
            customMenu.putClientProperty("removeOnWindowDeactivation", true);
            boardsCustomMenus.add(customMenu);
        }

        List<JMenuItem> menuItemsToClickAfterStartup = new LinkedList<>();

        ButtonGroup boardsButtonGroup = new ButtonGroup();
        Map<String, ButtonGroup> buttonGroupsMap = new HashMap<>();

        // Cycle through all packages
        boolean first = true;
        for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
            // For every package cycle through all platform
            for (TargetPlatform targetPlatform : targetPackage.platforms()) {

                // Add a separator from the previous platform
                if (!first)
                    boardMenu.add(new JSeparator());
                first = false;

                // Add a title for each platform
                String platformLabel = targetPlatform.getPreferences().get("name");
                if (platformLabel != null && !targetPlatform.getBoards().isEmpty()) {
                    JMenuItem menuLabel = new JMenuItem(tr(platformLabel));
                    menuLabel.setEnabled(false);
                    boardMenu.add(menuLabel);
                }

                // Cycle through all boards of this platform
                for (TargetBoard board : targetPlatform.getBoards().values()) {
                    if (board.getPreferences().get("hide") != null)
                        continue;
                    JMenuItem item = createBoardMenusAndCustomMenus(boardsCustomMenus, menuItemsToClickAfterStartup,
                            buttonGroupsMap, board, targetPlatform, targetPackage);
                    boardMenu.add(item);
                    boardsButtonGroup.add(item);
                }
            }
        }

        if (menuItemsToClickAfterStartup.isEmpty()) {
            menuItemsToClickAfterStartup.add(selectFirstEnabledMenuItem(boardMenu));
        }

        for (JMenuItem menuItemToClick : menuItemsToClickAfterStartup) {
            menuItemToClick.setSelected(true);
            menuItemToClick.getAction().actionPerformed(new ActionEvent(this, -1, ""));
        }
    }

    private JRadioButtonMenuItem createBoardMenusAndCustomMenus(final List<JMenu> boardsCustomMenus,
            List<JMenuItem> menuItemsToClickAfterStartup, Map<String, ButtonGroup> buttonGroupsMap,
            TargetBoard board, TargetPlatform targetPlatform, TargetPackage targetPackage) throws Exception {
        String selPackage = PreferencesData.get("target_package");
        String selPlatform = PreferencesData.get("target_platform");
        String selBoard = PreferencesData.get("board");

        String boardId = board.getId();
        String packageName = targetPackage.getId();
        String platformName = targetPlatform.getId();

        // Setup a menu item for the current board
        @SuppressWarnings("serial")
        Action action = new AbstractAction(board.getName()) {
            public void actionPerformed(ActionEvent actionevent) {
                BaseNoGui.selectBoard((TargetBoard) getValue("b"));
                filterVisibilityOfSubsequentBoardMenus(boardsCustomMenus, (TargetBoard) getValue("b"), 1);

                onBoardOrPortChange();
                rebuildImportMenu(Editor.importMenu);
                rebuildExamplesMenu(Editor.examplesMenu);
            }
        };
        action.putValue("b", board);

        JRadioButtonMenuItem item = new JRadioButtonMenuItem(action);

        if (selBoard.equals(boardId) && selPackage.equals(packageName) && selPlatform.equals(platformName)) {
            menuItemsToClickAfterStartup.add(item);
        }

        PreferencesMap customMenus = targetPlatform.getCustomMenus();
        for (final String menuId : customMenus.keySet()) {
            String title = customMenus.get(menuId);
            JMenu menu = getBoardCustomMenu(tr(title));

            if (board.hasMenu(menuId)) {
                PreferencesMap boardCustomMenu = board.getMenuLabels(menuId);
                for (String customMenuOption : boardCustomMenu.keySet()) {
                    @SuppressWarnings("serial")
                    Action subAction = new AbstractAction(tr(boardCustomMenu.get(customMenuOption))) {
                        public void actionPerformed(ActionEvent e) {
                            PreferencesData.set("custom_" + menuId, ((TargetBoard) getValue("board")).getId() + "_"
                                    + getValue("custom_menu_option"));
                            onBoardOrPortChange();
                        }
                    };
                    subAction.putValue("board", board);
                    subAction.putValue("custom_menu_option", customMenuOption);

                    if (!buttonGroupsMap.containsKey(menuId)) {
                        buttonGroupsMap.put(menuId, new ButtonGroup());
                    }

                    JRadioButtonMenuItem subItem = new JRadioButtonMenuItem(subAction);
                    menu.add(subItem);
                    buttonGroupsMap.get(menuId).add(subItem);

                    String selectedCustomMenuEntry = PreferencesData.get("custom_" + menuId);
                    if (selBoard.equals(boardId)
                            && (boardId + "_" + customMenuOption).equals(selectedCustomMenuEntry)) {
                        menuItemsToClickAfterStartup.add(subItem);
                    }
                }
            }
        }

        return item;
    }

    private void filterVisibilityOfSubsequentBoardMenus(List<JMenu> boardsCustomMenus, TargetBoard board,
            int fromIndex) {
        for (int i = fromIndex; i < boardsCustomMenus.size(); i++) {
            JMenu menu = boardsCustomMenus.get(i);
            for (int m = 0; m < menu.getItemCount(); m++) {
                JMenuItem menuItem = menu.getItem(m);
                menuItem.setVisible(menuItem.getAction().getValue("board").equals(board));
            }
            menu.setVisible(ifThereAreVisibleItemsOn(menu));

            if (menu.isVisible()) {
                JMenuItem visibleSelectedOrFirstMenuItem = selectVisibleSelectedOrFirstMenuItem(menu);
                if (!visibleSelectedOrFirstMenuItem.isSelected()) {
                    visibleSelectedOrFirstMenuItem.setSelected(true);
                    visibleSelectedOrFirstMenuItem.getAction().actionPerformed(null);
                }
            }
        }
    }

    private static boolean ifThereAreVisibleItemsOn(JMenu menu) {
        for (int i = 0; i < menu.getItemCount(); i++) {
            if (menu.getItem(i).isVisible()) {
                return true;
            }
        }
        return false;
    }

    private JMenu getBoardCustomMenu(String label) throws Exception {
        for (JMenu menu : boardsCustomMenus) {
            if (label.equals(menu.getText())) {
                return menu;
            }
        }
        throw new Exception("Custom menu not found!");
    }

    public List<JMenuItem> getProgrammerMenus() {
        return programmerMenus;
    }

    private static JMenuItem selectVisibleSelectedOrFirstMenuItem(JMenu menu) {
        JMenuItem firstVisible = null;
        for (int i = 0; i < menu.getItemCount(); i++) {
            JMenuItem item = menu.getItem(i);
            if (item != null && item.isVisible()) {
                if (item.isSelected()) {
                    return item;
                }
                if (firstVisible == null) {
                    firstVisible = item;
                }
            }
        }

        if (firstVisible != null) {
            return firstVisible;
        }

        throw new IllegalStateException("Menu has no enabled items");
    }

    private static JMenuItem selectFirstEnabledMenuItem(JMenu menu) {
        for (int i = 1; i < menu.getItemCount(); i++) {
            JMenuItem item = menu.getItem(i);
            if (item != null && item.isEnabled()) {
                return item;
            }
        }
        throw new IllegalStateException("Menu has no enabled items");
    }

    public void rebuildProgrammerMenu() {
        programmerMenus = new LinkedList<>();

        ButtonGroup group = new ButtonGroup();
        for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
            for (TargetPlatform targetPlatform : targetPackage.platforms()) {
                for (String programmer : targetPlatform.getProgrammers().keySet()) {
                    String id = targetPackage.getId() + ":" + programmer;

                    @SuppressWarnings("serial")
                    AbstractAction action = new AbstractAction(
                            targetPlatform.getProgrammer(programmer).get("name")) {
                        public void actionPerformed(ActionEvent actionevent) {
                            PreferencesData.set("programmer", "" + getValue("id"));
                        }
                    };
                    action.putValue("id", id);
                    JMenuItem item = new JRadioButtonMenuItem(action);
                    if (PreferencesData.get("programmer").equals(id)) {
                        item.setSelected(true);
                    }
                    group.add(item);
                    programmerMenus.add(item);
                }
            }
        }
    }

    /**
     * Scan a folder recursively, and add any sketches found to the menu
     * specified. Set the openReplaces parameter to true when opening the sketch
     * should replace the sketch in the current window, or false when the
     * sketch should open in a new window.
     */
    protected boolean addSketches(JMenu menu, File folder) {
        if (folder == null)
            return false;

        if (!folder.isDirectory())
            return false;

        File[] files = folder.listFiles();
        // If a bad folder or unreadable or whatever, this will come back null
        if (files == null)
            return false;

        // Alphabetize files, since it's not always alpha order
        Arrays.sort(files, new Comparator<File>() {
            @Override
            public int compare(File file, File file2) {
                return file.getName().compareToIgnoreCase(file2.getName());
            }
        });

        boolean ifound = false;

        for (File subfolder : files) {
            if (FileUtils.isSCCSOrHiddenFile(subfolder)) {
                continue;
            }

            if (!subfolder.isDirectory())
                continue;

            if (addSketchesSubmenu(menu, subfolder.getName(), subfolder)) {
                ifound = true;
            }
        }

        return ifound;
    }

    private boolean addSketchesSubmenu(JMenu menu, UserLibrary lib) {
        return addSketchesSubmenu(menu, lib.getName(), lib.getInstalledFolder());
    }

    private boolean addSketchesSubmenu(JMenu menu, String name, File folder) {

        ActionListener listener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                String path = e.getActionCommand();
                File file = new File(path);
                if (file.exists()) {
                    try {
                        handleOpen(file);
                    } catch (Exception e1) {
                        e1.printStackTrace();
                    }
                } else {
                    showWarning(tr("Sketch Does Not Exist"),
                            tr("The selected sketch no longer exists.\n"
                                    + "You may need to restart Arduino to update\n" + "the sketchbook menu."),
                            null);
                }
            }
        };

        File entry = new File(folder, name + ".ino");
        if (!entry.exists() && (new File(folder, name + ".pde")).exists())
            entry = new File(folder, name + ".pde");

        // if a .pde file of the same prefix as the folder exists..
        if (entry.exists()) {

            if (!BaseNoGui.isSanitaryName(name)) {
                if (!builtOnce) {
                    String complaining = I18n.format(
                            tr("The sketch \"{0}\" cannot be used.\n"
                                    + "Sketch names must contain only basic letters and numbers\n"
                                    + "(ASCII-only with no spaces, " + "and it cannot start with a number).\n"
                                    + "To get rid of this message, remove the sketch from\n" + "{1}"),
                            name, entry.getAbsolutePath());
                    showMessage(tr("Ignoring sketch with bad name"), complaining);
                }
                return false;
            }

            JMenuItem item = new JMenuItem(name);
            item.addActionListener(listener);
            item.setActionCommand(entry.getAbsolutePath());
            menu.add(item);
            return true;
        }

        // don't create an extra menu level for a folder named "examples"
        if (folder.getName().equals("examples"))
            return addSketches(menu, folder);

        // not a sketch folder, but maybe a subfolder containing sketches
        JMenu submenu = new JMenu(name);
        boolean found = addSketches(submenu, folder);
        if (found) {
            menu.add(submenu);
            MenuScroller.setScrollerFor(submenu);
        }
        return found;
    }

    protected void addLibraries(JMenu menu, LibraryList libs) throws IOException {

        LibraryList list = new LibraryList(libs);
        list.sort();

        for (UserLibrary lib : list) {
            @SuppressWarnings("serial")
            AbstractAction action = new AbstractAction(lib.getName()) {
                public void actionPerformed(ActionEvent event) {
                    UserLibrary l = (UserLibrary) getValue("library");
                    try {
                        activeEditor.getSketchController().importLibrary(l);
                    } catch (IOException e) {
                        showWarning(tr("Error"),
                                I18n.format("Unable to list header files in {0}", l.getSrcFolder()), e);
                    }
                }
            };
            action.putValue("library", lib);

            // Add new element at the bottom
            JMenuItem item = new JMenuItem(action);
            item.putClientProperty("library", lib);
            menu.add(item);

            // XXX: DAM: should recurse here so that library folders can be nested
        }
    }

    /**
     * Given a folder, return a list of the header files in that folder (but not
     * the header files in its sub-folders, as those should be included from
     * within the header files at the top-level).
     */
    static public String[] headerListFromIncludePath(File path) throws IOException {
        String[] list = path.list(new OnlyFilesWithExtension(".h"));
        if (list == null) {
            throw new IOException();
        }
        return list;
    }

    /**
     * Show the About box.
     */
    @SuppressWarnings("serial")
    public void handleAbout() {
        final Image image = Theme.getLibImage("about", activeEditor, Theme.scale(475), Theme.scale(300));
        final Window window = new Window(activeEditor) {
            public void paint(Graphics graphics) {
                Graphics2D g = Theme.setupGraphics2D(graphics);
                g.drawImage(image, 0, 0, null);

                Font f = new Font("SansSerif", Font.PLAIN, Theme.scale(11));
                g.setFont(f);
                g.setColor(new Color(0, 151, 156));
                g.drawString(BaseNoGui.VERSION_NAME_LONG, Theme.scale(33), Theme.scale(20));
            }
        };
        window.addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                window.dispose();
            }
        });
        int w = image.getWidth(activeEditor);
        int h = image.getHeight(activeEditor);
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        window.setBounds((screen.width - w) / 2, (screen.height - h) / 2, w, h);
        window.setLocationRelativeTo(activeEditor);
        window.setVisible(true);
    }

    /**
     * Show the Preferences window.
     */
    public void handlePrefs() {
        cc.arduino.view.preferences.Preferences dialog = new cc.arduino.view.preferences.Preferences(activeEditor,
                this);
        if (activeEditor != null) {
            dialog.setLocationRelativeTo(activeEditor);
        }
        dialog.setVisible(true);
    }

    /**
     * Adjust font size
     */
    public void handleFontSizeChange(int change) {
        String pieces[] = PreferencesData.get("editor.font").split(",");
        try {
            int newSize = Integer.parseInt(pieces[2]) + change;
            if (newSize < 4)
                newSize = 4;
            pieces[2] = String.valueOf(newSize);
        } catch (NumberFormatException e) {
            // ignore
            return;
        }
        PreferencesData.set("editor.font", StringUtils.join(pieces, ','));
        getEditors().forEach(Editor::applyPreferences);
    }

    public List<JMenu> getBoardsCustomMenus() {
        return boardsCustomMenus;
    }

    public File getDefaultSketchbookFolderOrPromptForIt() {
        File sketchbookFolder = BaseNoGui.getDefaultSketchbookFolder();

        if (sketchbookFolder == null && !isCommandLine()) {
            sketchbookFolder = promptSketchbookLocation();
        }

        // create the folder if it doesn't exist already
        boolean result = true;
        if (!sketchbookFolder.exists()) {
            result = sketchbookFolder.mkdirs();
        }

        if (!result) {
            showError(tr("You forgot your sketchbook"),
                    tr("Arduino cannot run because it could not\n" + "create a folder to store your sketchbook."),
                    null);
        }

        return sketchbookFolder;
    }

    /**
     * Check for a new sketchbook location.
     */
    static protected File promptSketchbookLocation() {
        File folder = null;

        folder = new File(System.getProperty("user.home"), "sketchbook");
        if (!folder.exists()) {
            folder.mkdirs();
            return folder;
        }

        String prompt = tr("Select (or create new) folder for sketches...");
        folder = selectFolder(prompt, null, null);
        if (folder == null) {
            System.exit(0);
        }
        return folder;
    }

    // .................................................................

    /**
     * Implements the cross-platform headache of opening URLs
     * TODO This code should be replaced by PApplet.link(),
     * however that's not a static method (because it requires
     * an AppletContext when used as an applet), so it's mildly
     * trickier than just removing this method.
     */
    static public void openURL(String url) {
        try {
            BaseNoGui.getPlatform().openURL(url);

        } catch (Exception e) {
            showWarning(tr("Problem Opening URL"), I18n.format(tr("Could not open the URL\n{0}"), url), e);
        }
    }

    /**
     * Used to determine whether to disable the "Show Sketch Folder" option.
     *
     * @return true If a means of opening a folder is known to be available.
     */
    static protected boolean openFolderAvailable() {
        return BaseNoGui.getPlatform().openFolderAvailable();
    }

    /**
     * Implements the other cross-platform headache of opening
     * a folder in the machine's native file browser.
     */
    static public void openFolder(File file) {
        try {
            BaseNoGui.getPlatform().openFolder(file);

        } catch (Exception e) {
            showWarning(tr("Problem Opening Folder"),
                    I18n.format(tr("Could not open the folder\n{0}"), file.getAbsolutePath()), e);
        }
    }

    // .................................................................

    static public File selectFolder(String prompt, File folder, Component parent) {
        JFileChooser fc = new JFileChooser();
        fc.setDialogTitle(prompt);
        if (folder != null) {
            fc.setSelectedFile(folder);
        }
        fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

        int returned = fc.showOpenDialog(parent);
        if (returned == JFileChooser.APPROVE_OPTION) {
            return fc.getSelectedFile();
        }
        return null;
    }

    // .................................................................

    /**
     * Give this Frame an icon.
     */
    static public void setIcon(Frame frame) {
        if (OSUtils.isMacOS()) {
            return;
        }

        List<Image> icons = Stream.of("16", "24", "32", "48", "64", "72", "96", "128", "256")
                .map(res -> "/lib/icons/" + res + "x" + res + "/apps/arduino.png")
                .map(path -> BaseNoGui.getContentFile(path).getAbsolutePath())
                .map(absPath -> Toolkit.getDefaultToolkit().createImage(absPath)).collect(Collectors.toList());
        frame.setIconImages(icons);
    }

    /**
     * Registers key events for a Ctrl-W and ESC with an ActionListener
     * that will take care of disposing the window.
     */
    static public void registerWindowCloseKeys(JRootPane root, ActionListener disposer) {
        KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
        root.registerKeyboardAction(disposer, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);

        int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
        stroke = KeyStroke.getKeyStroke('W', modifiers);
        root.registerKeyboardAction(disposer, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    // .................................................................

    static public void showReference(String filename) {
        showReference("reference/www.arduino.cc/en", filename);
    }

    static public void showReference(String prefix, String filename) {
        File referenceFolder = getContentFile(prefix);
        File referenceFile = new File(referenceFolder, filename);
        if (!referenceFile.exists())
            referenceFile = new File(referenceFolder, filename + ".html");

        if (referenceFile.exists()) {
            openURL(referenceFile.getAbsolutePath());
        } else {
            showWarning(tr("Problem Opening URL"), I18n.format(tr("Could not open the URL\n{0}"), referenceFile),
                    null);
        }
    }

    public static void showEdisonGettingStarted() {
        showReference("reference/Edison_help_files", "ArduinoIDE_guide_edison");
    }

    static public void showArduinoGettingStarted() {
        if (OSUtils.isMacOS()) {
            showReference("Guide/MacOSX");
        } else if (OSUtils.isWindows()) {
            showReference("Guide/Windows");
        } else {
            openURL("http://www.arduino.cc/playground/Learning/Linux");
        }
    }

    static public void showReference() {
        showReference("Reference/HomePage");
    }

    static public void showEnvironment() {
        showReference("Guide/Environment");
    }

    static public void showTroubleshooting() {
        showReference("Guide/Troubleshooting");
    }

    static public void showFAQ() {
        showReference("Main/FAQ");
    }

    // .................................................................

    /**
     * "No cookie for you" type messages. Nothing fatal or all that
     * much of a bummer, but something to notify the user about.
     */
    static public void showMessage(String title, String message) {
        BaseNoGui.showMessage(title, message);
    }

    /**
     * Non-fatal error message with optional stack trace side dish.
     */
    static public void showWarning(String title, String message, Exception e) {
        BaseNoGui.showWarning(title, message, e);
    }

    static public void showError(String title, String message, Throwable e) {
        showError(title, message, e, 1);
    }

    static public void showError(String title, String message, int exit_code) {
        showError(title, message, null, exit_code);
    }

    /**
     * Show an error message that's actually fatal to the program.
     * This is an error that can't be recovered. Use showWarning()
     * for errors that allow P5 to continue running.
     */
    static public void showError(String title, String message, Throwable e, int exit_code) {
        BaseNoGui.showError(title, message, e, exit_code);
    }

    static public File getContentFile(String name) {
        return BaseNoGui.getContentFile(name);
    }

    // ...................................................................

    /**
     * Get the number of lines in a file by counting the number of newline
     * characters inside a String (and adding 1).
     */
    static public int countLines(String what) {
        return BaseNoGui.countLines(what);
    }

    /**
     * Same as PApplet.loadBytes(), however never does gzip decoding.
     */
    static public byte[] loadBytesRaw(File file) throws IOException {
        int size = (int) file.length();
        FileInputStream input = null;
        try {
            input = new FileInputStream(file);
            byte buffer[] = new byte[size];
            int offset = 0;
            int bytesRead;
            while ((bytesRead = input.read(buffer, offset, size - offset)) != -1) {
                offset += bytesRead;
                if (bytesRead == 0)
                    break;
            }
            return buffer;
        } finally {
            IOUtils.closeQuietly(input);
        }
    }

    /**
     * Read from a file with a bunch of attribute/value pairs
     * that are separated by = and ignore comments with #.
     */
    static public HashMap<String, String> readSettings(File inputFile) {
        HashMap<String, String> outgoing = new HashMap<>();
        if (!inputFile.exists())
            return outgoing; // return empty hash

        String lines[] = PApplet.loadStrings(inputFile);
        for (int i = 0; i < lines.length; i++) {
            int hash = lines[i].indexOf('#');
            String line = (hash == -1) ? lines[i].trim() : lines[i].substring(0, hash).trim();
            if (line.length() == 0)
                continue;

            int equals = line.indexOf('=');
            if (equals == -1) {
                System.err.println("ignoring illegal line in " + inputFile);
                System.err.println("  " + line);
                continue;
            }
            String attr = line.substring(0, equals).trim();
            String valu = line.substring(equals + 1).trim();
            outgoing.put(attr, valu);
        }
        return outgoing;
    }

    static public void copyFile(File sourceFile, File targetFile) throws IOException {
        InputStream from = null;
        OutputStream to = null;
        try {
            from = new BufferedInputStream(new FileInputStream(sourceFile));
            to = new BufferedOutputStream(new FileOutputStream(targetFile));
            byte[] buffer = new byte[16 * 1024];
            int bytesRead;
            while ((bytesRead = from.read(buffer)) != -1) {
                to.write(buffer, 0, bytesRead);
            }
            to.flush();
        } finally {
            IOUtils.closeQuietly(from);
            IOUtils.closeQuietly(to);
        }

        targetFile.setLastModified(sourceFile.lastModified());
    }

    /**
     * Grab the contents of a file as a string.
     */
    static public String loadFile(File file) throws IOException {
        return BaseNoGui.loadFile(file);
    }

    /**
     * Spew the contents of a String object out to a file.
     */
    static public void saveFile(String str, File file) throws IOException {
        BaseNoGui.saveFile(str, file);
    }

    /**
     * Calculate the size of the contents of a folder.
     * Used to determine whether sketches are empty or not.
     * Note that the function calls itself recursively.
     */
    static public int calcFolderSize(File folder) {
        int size = 0;

        String files[] = folder.list();
        // null if folder doesn't exist, happens when deleting sketch
        if (files == null)
            return -1;

        for (int i = 0; i < files.length; i++) {
            if (files[i].equals(".") || (files[i].equals("..")) || files[i].equals(".DS_Store"))
                continue;
            File fella = new File(folder, files[i]);
            if (fella.isDirectory()) {
                size += calcFolderSize(fella);
            } else {
                size += (int) fella.length();
            }
        }
        return size;
    }

    public void handleAddLibrary() {
        JFileChooser fileChooser = new JFileChooser(System.getProperty("user.home"));
        fileChooser.setDialogTitle(tr("Select a zip file or a folder containing the library you'd like to add"));
        fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        fileChooser.setFileFilter(new FileNameExtensionFilter(tr("ZIP files or folders"), "zip"));

        Dimension preferredSize = fileChooser.getPreferredSize();
        fileChooser.setPreferredSize(new Dimension(preferredSize.width + 200, preferredSize.height + 200));

        int returnVal = fileChooser.showOpenDialog(activeEditor);

        if (returnVal != JFileChooser.APPROVE_OPTION) {
            return;
        }

        File sourceFile = fileChooser.getSelectedFile();
        File tmpFolder = null;

        try {
            // unpack ZIP
            if (!sourceFile.isDirectory()) {
                try {
                    tmpFolder = FileUtils.createTempFolder();
                    ZipDeflater zipDeflater = new ZipDeflater(sourceFile, tmpFolder);
                    zipDeflater.deflate();
                    File[] foldersInTmpFolder = tmpFolder.listFiles(new OnlyDirs());
                    if (foldersInTmpFolder.length != 1) {
                        throw new IOException(tr("Zip doesn't contain a library"));
                    }
                    sourceFile = foldersInTmpFolder[0];
                } catch (IOException e) {
                    activeEditor.statusError(e);
                    return;
                }
            }

            File libFolder = sourceFile;
            if (FileUtils.isSubDirectory(new File(PreferencesData.get("sketchbook.path")), libFolder)) {
                activeEditor.statusError(tr("A subfolder of your sketchbook is not a valid library"));
                return;
            }

            if (FileUtils.isSubDirectory(libFolder, new File(PreferencesData.get("sketchbook.path")))) {
                activeEditor.statusError(tr("You can't import a folder that contains your sketchbook"));
                return;
            }

            String libName = libFolder.getName();
            if (!BaseNoGui.isSanitaryName(libName)) {
                String mess = I18n.format(tr("The library \"{0}\" cannot be used.\n"
                        + "Library names must contain only basic letters and numbers.\n"
                        + "(ASCII only and no spaces, and it cannot start with a number)"), libName);
                activeEditor.statusError(mess);
                return;
            }

            String[] headers;
            File libProp = new File(libFolder, "library.properties");
            File srcFolder = new File(libFolder, "src");
            if (libProp.exists() && srcFolder.isDirectory()) {
                headers = BaseNoGui.headerListFromIncludePath(srcFolder);
            } else {
                headers = BaseNoGui.headerListFromIncludePath(libFolder);
            }
            if (headers.length == 0) {
                activeEditor.statusError(tr("Specified folder/zip file does not contain a valid library"));
                return;
            }

            // copy folder
            File destinationFolder = new File(BaseNoGui.getSketchbookLibrariesFolder().folder,
                    sourceFile.getName());
            if (!destinationFolder.mkdir()) {
                activeEditor
                        .statusError(I18n.format(tr("A library named {0} already exists"), sourceFile.getName()));
                return;
            }
            try {
                FileUtils.copy(sourceFile, destinationFolder);
            } catch (IOException e) {
                activeEditor.statusError(e);
                return;
            }
            activeEditor.statusNotice(tr("Library added to your libraries. Check \"Include library\" menu"));
        } catch (IOException e) {
            // FIXME error when importing. ignoring :(
        } finally {
            // delete zip created temp folder, if exists
            newLibraryImported = true;
            FileUtils.recursiveDelete(tmpFolder);
        }
    }

    public static DiscoveryManager getDiscoveryManager() {
        return BaseNoGui.getDiscoveryManager();
    }

    public Editor getActiveEditor() {
        return activeEditor;
    }

    public boolean hasActiveEditor() {
        return activeEditor != null;
    }

    public List<Editor> getEditors() {
        return new LinkedList<>(editors);
    }

    public PdeKeywords getPdeKeywords() {
        return pdeKeywords;
    }

    public List<JMenuItem> getRecentSketchesMenuItems() {
        return recentSketchesMenuItems;
    }

}