processing.app.Sketch.java Source code

Java tutorial

Introduction

Here is the source code for processing.app.Sketch.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 as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.
    
  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.CompilerProgressListener;
import cc.arduino.UploaderUtils;
import cc.arduino.files.DeleteFilesOnShutdown;
import cc.arduino.packages.Uploader;
import org.apache.commons.codec.digest.DigestUtils;
import processing.app.debug.RunnerException;
import processing.app.forms.PasswordAuthorizationDialog;
import processing.app.helpers.FileUtils;
import processing.app.helpers.OSUtils;
import processing.app.helpers.PreferencesMapException;
import processing.app.packages.LibraryList;
import processing.app.packages.UserLibrary;

import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static processing.app.I18n.tr;

/**
 * Stores information about files in the current sketch
 */
public class Sketch {
    private final Editor editor;

    /** true if any of the files have been modified. */
    private boolean modified;

    private SketchCodeDocument current;
    private int currentIndex;

    private final SketchData data;

    /**
     * path is location of the main .pde file, because this is also
     * simplest to use when opening the file from the finder/explorer.
     */
    public Sketch(Editor _editor, File file) throws IOException {
        editor = _editor;
        data = new SketchData(file);
        load();
    }

    /**
     * Build the list of files.
     * <P>
     * Generally this is only done once, rather than
     * each time a change is made, because otherwise it gets to be
     * a nightmare to keep track of what files went where, because
     * not all the data will be saved to disk.
     * <P>
     * This also gets called when the main sketch file is renamed,
     * because the sketch has to be reloaded from a different folder.
     * <P>
     * Another exception is when an external editor is in use,
     * in which case the load happens each time "run" is hit.
     */
    private void load() throws IOException {
        load(false);
    }

    protected void load(boolean forceUpdate) throws IOException {
        data.load();

        for (SketchCode code : data.getCodes()) {
            if (code.getMetadata() == null)
                code.setMetadata(new SketchCodeDocument(this, code));
        }

        // set the main file to be the current tab
        if (editor != null) {
            setCurrentCode(currentIndex, forceUpdate);
        }
    }

    private boolean renamingCode;

    /**
     * Handler for the New Code menu option.
     */
    public void handleNewCode() {
        editor.status.clearState();
        // make sure the user didn't hide the sketch folder
        ensureExistence();

        // if read-only, give an error
        if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
            // if the files are read-only, need to first do a "save as".
            Base.showMessage(tr("Sketch is Read-Only"), tr("Some files are marked \"read-only\", so you'll\n"
                    + "need to re-save the sketch in another location,\n" + "and try again."));
            return;
        }

        renamingCode = false;
        editor.status.edit(tr("Name for new file:"), "");
    }

    /**
     * Handler for the Rename Code menu option.
     */
    public void handleRenameCode() {
        editor.status.clearState();
        // make sure the user didn't hide the sketch folder
        ensureExistence();

        if (currentIndex == 0 && editor.untitled) {
            Base.showMessage(tr("Sketch is Untitled"),
                    tr("How about saving the sketch first \n" + "before trying to rename it?"));
            return;
        }

        // if read-only, give an error
        if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
            // if the files are read-only, need to first do a "save as".
            Base.showMessage(tr("Sketch is Read-Only"), tr("Some files are marked \"read-only\", so you'll\n"
                    + "need to re-save the sketch in another location,\n" + "and try again."));
            return;
        }

        // ask for new name of file (internal to window)
        // TODO maybe just popup a text area?
        renamingCode = true;
        String prompt = (currentIndex == 0) ? "New name for sketch:" : "New name for file:";
        String oldName = (current.getCode().isExtension("ino")) ? current.getCode().getPrettyName()
                : current.getCode().getFileName();
        editor.status.edit(prompt, oldName);
    }

    /**
     * This is called upon return from entering a new file name.
     * (that is, from either newCode or renameCode after the prompt)
     * This code is almost identical for both the newCode and renameCode
     * cases, so they're kept merged except for right in the middle
     * where they diverge.
     */
    protected void nameCode(String newName) {
        // make sure the user didn't hide the sketch folder
        ensureExistence();

        // Add the extension here, this simplifies some of the logic below.
        if (newName.indexOf('.') == -1) {
            newName += "." + getDefaultExtension();
        }

        // if renaming to the same thing as before, just ignore.
        // also ignoring case here, because i don't want to write
        // a bunch of special stuff for each platform
        // (osx is case insensitive but preserving, windows insensitive,
        // *nix is sensitive and preserving.. argh)
        if (renamingCode) {
            if (newName.equalsIgnoreCase(current.getCode().getFileName()) && OSUtils.isWindows()) {
                // exit quietly for the 'rename' case.
                // if it's a 'new' then an error will occur down below
                return;
            }
        }

        newName = newName.trim();
        if (newName.equals(""))
            return;

        int dot = newName.indexOf('.');
        if (dot == 0) {
            Base.showWarning(tr("Problem with rename"), tr("The name cannot start with a period."), null);
            return;
        }

        String newExtension = newName.substring(dot + 1).toLowerCase();
        if (!validExtension(newExtension)) {
            Base.showWarning(tr("Problem with rename"),
                    I18n.format(tr("\".{0}\" is not a valid extension."), newExtension), null);
            return;
        }

        // Don't let the user create the main tab as a .java file instead of .pde
        if (!isDefaultExtension(newExtension)) {
            if (renamingCode) { // If creating a new tab, don't show this error
                if (current.getCode() == data.getCode(0)) { // If this is the main tab, disallow
                    Base.showWarning(tr("Problem with rename"), tr("The main file can't use an extension.\n"
                            + "(It may be time for your to graduate to a\n" + "\"real\" programming environment)"),
                            null);
                    return;
                }
            }
        }

        // dots are allowed for the .pde and .java, but not in the name
        // make sure the user didn't name things poo.time.pde
        // or something like that (nothing against poo time)
        String shortName = newName.substring(0, dot);
        String sanitaryName = BaseNoGui.sanitizeName(shortName);
        if (!shortName.equals(sanitaryName)) {
            newName = sanitaryName + "." + newExtension;
        }

        // In Arduino, we want to allow files with the same name but different
        // extensions, so compare the full names (including extensions).  This
        // might cause problems: http://dev.processing.org/bugs/show_bug.cgi?id=543
        for (SketchCode c : data.getCodes()) {
            if (newName.equalsIgnoreCase(c.getFileName()) && OSUtils.isWindows()) {
                Base.showMessage(tr("Error"), I18n.format(tr("A file named \"{0}\" already exists in \"{1}\""),
                        c.getFileName(), data.getFolder().getAbsolutePath()));
                return;
            }
        }

        // In Arduino, don't allow a .cpp file with the same name as the sketch,
        // because the sketch is concatenated into a file with that name as part
        // of the build process.  
        if (newName.equals(getName() + ".cpp")) {
            Base.showMessage(tr("Error"), tr("You can't have a .cpp file with the same name as the sketch."));
            return;
        }

        if (renamingCode && currentIndex == 0) {
            for (SketchCode code : data.getCodes()) {
                if (sanitaryName.equalsIgnoreCase(code.getPrettyName()) && code.isExtension("cpp")) {
                    Base.showMessage(tr("Error"),
                            I18n.format(
                                    tr("You can't rename the sketch to \"{0}\"\n"
                                            + "because the sketch already has a .cpp file with that name."),
                                    sanitaryName));
                    return;
                }
            }
        }

        File newFile = new File(data.getFolder(), newName);
        //    if (newFile.exists()) {  // yay! users will try anything
        //      Base.showMessage("Error",
        //                       "A file named \"" + newFile + "\" already exists\n" +
        //                       "in \"" + folder.getAbsolutePath() + "\"");
        //      return;
        //    }

        //    File newFileHidden = new File(folder, newName + ".x");
        //    if (newFileHidden.exists()) {
        //      // don't let them get away with it if they try to create something
        //      // with the same name as something hidden
        //      Base.showMessage("No Way",
        //                       "A hidden tab with the same name already exists.\n" +
        //                       "Use \"Unhide\" to bring it back.");
        //      return;
        //    }

        if (renamingCode) {
            if (currentIndex == 0) {
                // get the new folder name/location
                String folderName = newName.substring(0, newName.indexOf('.'));
                File newFolder = new File(data.getFolder().getParentFile(), folderName);
                if (newFolder.exists()) {
                    Base.showWarning(tr("Cannot Rename"), I18n.format(
                            tr("Sorry, a sketch (or folder) named " + "\"{0}\" already exists."), newName), null);
                    return;
                }

                // unfortunately this can't be a "save as" because that
                // only copies the sketch files and the data folder
                // however this *will* first save the sketch, then rename

                // first get the contents of the editor text area
                if (current.getCode().isModified()) {
                    current.getCode().setProgram(editor.getText());
                    try {
                        // save this new SketchCode
                        current.getCode().save();
                    } catch (Exception e) {
                        Base.showWarning(tr("Error"), tr("Could not rename the sketch. (0)"), e);
                        return;
                    }
                }

                if (!current.getCode().renameTo(newFile)) {
                    Base.showWarning(tr("Error"), I18n.format(tr("Could not rename \"{0}\" to \"{1}\""),
                            current.getCode().getFileName(), newFile.getName()), null);
                    return;
                }

                // save each of the other tabs because this is gonna be re-opened
                try {
                    for (SketchCode code : data.getCodes()) {
                        code.save();
                    }
                } catch (Exception e) {
                    Base.showWarning(tr("Error"), tr("Could not rename the sketch. (1)"), e);
                    return;
                }

                // now rename the sketch folder and re-open
                boolean success = data.getFolder().renameTo(newFolder);
                if (!success) {
                    Base.showWarning(tr("Error"), tr("Could not rename the sketch. (2)"), null);
                    return;
                }
                // if successful, set base properties for the sketch

                File newMainFile = new File(newFolder, newName + ".ino");

                // having saved everything and renamed the folder and the main .pde,
                // use the editor to re-open the sketch to re-init state
                // (unfortunately this will kill positions for carets etc)
                editor.handleOpenUnchecked(newMainFile, currentIndex, editor.getSelectionStart(),
                        editor.getSelectionStop(), editor.getScrollPosition());

                // get the changes into the sketchbook menu
                // (re-enabled in 0115 to fix bug #332)
                editor.base.rebuildSketchbookMenus();

            } else { // else if something besides code[0]
                if (!current.getCode().renameTo(newFile)) {
                    Base.showWarning(tr("Error"), I18n.format(tr("Could not rename \"{0}\" to \"{1}\""),
                            current.getCode().getFileName(), newFile.getName()), null);
                    return;
                }
            }

        } else { // creating a new file
            try {
                if (!newFile.createNewFile()) {
                    // Already checking for IOException, so make our own.
                    throw new IOException(tr("createNewFile() returned false"));
                }
            } catch (IOException e) {
                Base.showWarning(tr("Error"), I18n.format("Could not create the file \"{0}\" in \"{1}\"", newFile,
                        data.getFolder().getAbsolutePath()), e);
                return;
            }
            ensureExistence();
            data.addCode((new SketchCodeDocument(this, newFile)).getCode());
        }

        // sort the entries
        data.sortCode();

        // set the new guy as current
        setCurrentCode(newName);

        // update the tabs
        editor.header.rebuild();
    }

    /**
     * Remove a piece of code from the sketch and from the disk.
     */
    public void handleDeleteCode() throws IOException {
        editor.status.clearState();
        // make sure the user didn't hide the sketch folder
        ensureExistence();

        // if read-only, give an error
        if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
            // if the files are read-only, need to first do a "save as".
            Base.showMessage(tr("Sketch is Read-Only"), tr("Some files are marked \"read-only\", so you'll\n"
                    + "need to re-save the sketch in another location,\n" + "and try again."));
            return;
        }

        // confirm deletion with user, yes/no
        Object[] options = { tr("OK"), tr("Cancel") };
        String prompt = (currentIndex == 0) ? tr("Are you sure you want to delete this sketch?")
                : I18n.format(tr("Are you sure you want to delete \"{0}\"?"),
                        current.getCode().getFileNameWithExtensionIfNotIno());
        int result = JOptionPane.showOptionDialog(editor, prompt, tr("Delete"), JOptionPane.YES_NO_OPTION,
                JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
        if (result == JOptionPane.YES_OPTION) {
            if (currentIndex == 0) {
                // need to unset all the modified flags, otherwise tries
                // to do a save on the handleNew()

                // delete the entire sketch
                Base.removeDir(data.getFolder());

                // get the changes into the sketchbook menu
                //sketchbook.rebuildMenus();

                // make a new sketch, and i think this will rebuild the sketch menu
                //editor.handleNewUnchecked();
                //editor.handleClose2();
                editor.base.handleClose(editor);

            } else {
                // delete the file
                if (!current.getCode().deleteFile(BaseNoGui.getBuildFolder(data))) {
                    Base.showMessage(tr("Couldn't do it"),
                            I18n.format(tr("Could not delete \"{0}\"."), current.getCode().getFileName()));
                    return;
                }

                // remove code from the list
                data.removeCode(current.getCode());

                // just set current tab to the main tab
                setCurrentCode(0);

                // update the tabs
                editor.header.repaint();
            }
        }
    }

    /**
     * Move to the previous tab.
     */
    public void handlePrevCode() {
        int prev = currentIndex - 1;
        if (prev < 0)
            prev = data.getCodeCount() - 1;
        setCurrentCode(prev);
    }

    /**
     * Move to the next tab.
     */
    public void handleNextCode() {
        setCurrentCode((currentIndex + 1) % data.getCodeCount());
    }

    /**
     * Sets the modified value for the code in the frontmost tab.
     */
    public void setModified(boolean state) {
        //System.out.println("setting modified to " + state);
        //new Exception().printStackTrace();
        current.getCode().setModified(state);
        calcModified();
    }

    private void calcModified() {
        modified = false;
        for (SketchCode code : data.getCodes()) {
            if (code.isModified()) {
                modified = true;
                break;
            }
        }
        editor.header.repaint();

        if (OSUtils.isMacOS()) {
            // http://developer.apple.com/qa/qa2001/qa1146.html
            Object modifiedParam = modified ? Boolean.TRUE : Boolean.FALSE;
            editor.getRootPane().putClientProperty("windowModified", modifiedParam);
            editor.getRootPane().putClientProperty("Window.documentModified", modifiedParam);
        }
    }

    public boolean isModified() {
        return modified;
    }

    /**
     * Save all code in the current sketch.
     */
    public boolean save() throws IOException {
        // make sure the user didn't hide the sketch folder
        ensureExistence();

        // first get the contents of the editor text area
        if (current.getCode().isModified()) {
            current.getCode().setProgram(editor.getText());
        }

        // don't do anything if not actually modified
        //if (!modified) return false;

        if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
            Base.showMessage(tr("Sketch is read-only"), tr("Some files are marked \"read-only\", so you'll\n"
                    + "need to re-save this sketch to another location."));
            return saveAs();
        }

        // rename .pde files to .ino
        File mainFile = new File(getMainFilePath());
        File mainFolder = mainFile.getParentFile();
        File[] pdeFiles = mainFolder.listFiles((dir, name) -> {
            return name.toLowerCase().endsWith(".pde");
        });

        if (pdeFiles != null && pdeFiles.length > 0) {
            if (PreferencesData.get("editor.update_extension") == null) {
                Object[] options = { tr("OK"), tr("Cancel") };
                int result = JOptionPane.showOptionDialog(editor,
                        tr("In Arduino 1.0, the default file extension has changed\n"
                                + "from .pde to .ino.  New sketches (including those created\n"
                                + "by \"Save-As\") will use the new extension.  The extension\n"
                                + "of existing sketches will be updated on save, but you can\n"
                                + "disable this in the Preferences dialog.\n" + "\n"
                                + "Save sketch and update its extension?"),
                        tr(".pde -> .ino"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
                        options, options[0]);

                if (result != JOptionPane.OK_OPTION)
                    return false; // save cancelled

                PreferencesData.setBoolean("editor.update_extension", true);
            }

            if (PreferencesData.getBoolean("editor.update_extension")) {
                // Do rename of all .pde files to new .ino extension
                for (File pdeFile : pdeFiles)
                    renameCodeToInoExtension(pdeFile);
            }
        }

        data.save();
        calcModified();
        return true;
    }

    private boolean renameCodeToInoExtension(File pdeFile) {
        for (SketchCode c : data.getCodes()) {
            if (!c.getFile().equals(pdeFile))
                continue;

            String pdeName = pdeFile.getPath();
            pdeName = pdeName.substring(0, pdeName.length() - 4) + ".ino";
            return c.renameTo(new File(pdeName));
        }
        return false;
    }

    /**
     * Handles 'Save As' for a sketch.
     * <P>
     * This basically just duplicates the current sketch folder to
     * a new location, and then calls 'Save'. (needs to take the current
     * state of the open files and save them to the new folder..
     * but not save over the old versions for the old sketch..)
     * <P>
     * Also removes the previously-generated .class and .jar files,
     * because they can cause trouble.
     */
    protected boolean saveAs() throws IOException {
        // get new name for folder
        FileDialog fd = new FileDialog(editor, tr("Save sketch folder as..."), FileDialog.SAVE);
        if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())
                || isUntitled()) {
            // default to the sketchbook folder
            fd.setDirectory(BaseNoGui.getSketchbookFolder().getAbsolutePath());
        } else {
            // default to the parent folder of where this was
            // on macs a .getParentFile() method is required

            fd.setDirectory(data.getFolder().getParentFile().getAbsolutePath());
        }
        String oldName = data.getName();
        fd.setFile(oldName);

        fd.setVisible(true);
        String newParentDir = fd.getDirectory();
        String newName = fd.getFile();

        // user canceled selection
        if (newName == null)
            return false;
        newName = Sketch.checkName(newName);

        File newFolder = new File(newParentDir, newName);

        // make sure there doesn't exist a .cpp file with that name already
        // but ignore this situation for the first tab, since it's probably being
        // resaved (with the same name) to another location/folder.
        for (int i = 1; i < data.getCodeCount(); i++) {
            SketchCode code = data.getCode(i);
            if (newName.equalsIgnoreCase(code.getPrettyName())) {
                Base.showMessage(tr("Error"), I18n.format(tr("You can't save the sketch as \"{0}\"\n"
                        + "because the sketch already has a file with that name."), newName));
                return false;
            }
        }

        // check if the paths are identical
        if (newFolder.equals(data.getFolder())) {
            // just use "save" here instead, because the user will have received a
            // message (from the operating system) about "do you want to replace?"
            return save();
        }

        // check to see if the user is trying to save this sketch inside itself
        try {
            String newPath = newFolder.getCanonicalPath() + File.separator;
            String oldPath = data.getFolder().getCanonicalPath() + File.separator;

            if (newPath.indexOf(oldPath) == 0) {
                Base.showWarning(tr("How very Borges of you"), tr(
                        "You cannot save the sketch into a folder\n" + "inside itself. This would go on forever."),
                        null);
                return false;
            }
        } catch (IOException e) {
            //ignore
        }

        // if the new folder already exists, then need to remove
        // its contents before copying everything over
        // (user will have already been warned)
        if (newFolder.exists()) {
            Base.removeDir(newFolder);
        }
        // in fact, you can't do this on windows because the file dialog
        // will instead put you inside the folder, but it happens on osx a lot.

        // now make a fresh copy of the folder
        newFolder.mkdirs();

        // grab the contents of the current tab before saving
        // first get the contents of the editor text area
        if (current.getCode().isModified()) {
            current.getCode().setProgram(editor.getText());
        }

        // save the other tabs to their new location
        for (SketchCode code : data.getCodes()) {
            if (data.indexOfCode(code) == 0)
                continue;
            File newFile = new File(newFolder, code.getFileName());
            code.saveAs(newFile);
        }

        // re-copy the data folder (this may take a while.. add progress bar?)
        if (data.getDataFolder().exists()) {
            File newDataFolder = new File(newFolder, "data");
            Base.copyDir(data.getDataFolder(), newDataFolder);
        }

        // re-copy the code folder
        if (data.getCodeFolder().exists()) {
            File newCodeFolder = new File(newFolder, "code");
            Base.copyDir(data.getCodeFolder(), newCodeFolder);
        }

        // copy custom applet.html file if one exists
        // http://dev.processing.org/bugs/show_bug.cgi?id=485
        File customHtml = new File(data.getFolder(), "applet.html");
        if (customHtml.exists()) {
            File newHtml = new File(newFolder, "applet.html");
            Base.copyFile(customHtml, newHtml);
        }

        // save the main tab with its new name
        File newFile = new File(newFolder, newName + ".ino");
        data.getCode(0).saveAs(newFile);

        editor.handleOpenUnchecked(newFile, currentIndex, editor.getSelectionStart(), editor.getSelectionStop(),
                editor.getScrollPosition());

        // Name changed, rebuild the sketch menus
        //editor.sketchbook.rebuildMenusAsync();
        editor.base.rebuildSketchbookMenus();

        // Make sure that it's not an untitled sketch
        setUntitled(false);

        // let Editor know that the save was successful
        return true;
    }

    /**
     * Prompt the user for a new file to the sketch, then call the
     * other addFile() function to actually add it.
     */
    public void handleAddFile() {
        // make sure the user didn't hide the sketch folder
        ensureExistence();

        // if read-only, give an error
        if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
            // if the files are read-only, need to first do a "save as".
            Base.showMessage(tr("Sketch is Read-Only"), tr("Some files are marked \"read-only\", so you'll\n"
                    + "need to re-save the sketch in another location,\n" + "and try again."));
            return;
        }

        // get a dialog, select a file to add to the sketch
        FileDialog fd = new FileDialog(editor, tr("Select an image or other data file to copy to your sketch"),
                FileDialog.LOAD);
        fd.setVisible(true);

        String directory = fd.getDirectory();
        String filename = fd.getFile();
        if (filename == null)
            return;

        // copy the file into the folder. if people would rather
        // it move instead of copy, they can do it by hand
        File sourceFile = new File(directory, filename);

        // now do the work of adding the file
        boolean result = addFile(sourceFile);

        if (result) {
            editor.statusNotice(tr("One file added to the sketch."));
            PreferencesData.set("last.folder", sourceFile.getAbsolutePath());
        }
    }

    /**
     * Add a file to the sketch.
     * <p/>
     * .pde or .java files will be added to the sketch folder. <br/>
     * .jar, .class, .dll, .jnilib, and .so files will all
     * be added to the "code" folder. <br/>
     * All other files will be added to the "data" folder.
     * <p/>
     * If they don't exist already, the "code" or "data" folder
     * will be created.
     * <p/>
     * @return true if successful.
     */
    public boolean addFile(File sourceFile) {
        String filename = sourceFile.getName();
        File destFile = null;
        String codeExtension = null;
        boolean replacement = false;

        // if the file appears to be code related, drop it
        // into the code folder, instead of the data folder
        if (filename.toLowerCase().endsWith(".o") || filename.toLowerCase().endsWith(".a")
                || filename.toLowerCase().endsWith(".so")) {

            //if (!codeFolder.exists()) codeFolder.mkdirs();
            prepareCodeFolder();
            destFile = new File(data.getCodeFolder(), filename);

        } else {
            for (String extension : SketchData.EXTENSIONS) {
                String lower = filename.toLowerCase();
                if (lower.endsWith("." + extension)) {
                    destFile = new File(data.getFolder(), filename);
                    codeExtension = extension;
                }
            }
            if (codeExtension == null) {
                prepareDataFolder();
                destFile = new File(data.getDataFolder(), filename);
            }
        }

        // check whether this file already exists
        if (destFile.exists()) {
            Object[] options = { tr("OK"), tr("Cancel") };
            String prompt = I18n.format(tr("Replace the existing version of {0}?"), filename);
            int result = JOptionPane.showOptionDialog(editor, prompt, tr("Replace"), JOptionPane.YES_NO_OPTION,
                    JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
            if (result == JOptionPane.YES_OPTION) {
                replacement = true;
            } else {
                return false;
            }
        }

        // If it's a replacement, delete the old file first,
        // otherwise case changes will not be preserved.
        // http://dev.processing.org/bugs/show_bug.cgi?id=969
        if (replacement) {
            boolean muchSuccess = destFile.delete();
            if (!muchSuccess) {
                Base.showWarning(tr("Error adding file"),
                        I18n.format(tr("Could not delete the existing ''{0}'' file."), filename), null);
                return false;
            }
        }

        // make sure they aren't the same file
        if ((codeExtension == null) && sourceFile.equals(destFile)) {
            Base.showWarning(tr("You can't fool me"),
                    tr("This file has already been copied to the\n"
                            + "location from which where you're trying to add it.\n" + "I ain't not doin nuthin'."),
                    null);
            return false;
        }

        // in case the user is "adding" the code in an attempt
        // to update the sketch's tabs
        if (!sourceFile.equals(destFile)) {
            try {
                Base.copyFile(sourceFile, destFile);

            } catch (IOException e) {
                Base.showWarning(tr("Error adding file"),
                        I18n.format(tr("Could not add ''{0}'' to the sketch."), filename), e);
                return false;
            }
        }

        if (codeExtension != null) {
            SketchCode newCode = (new SketchCodeDocument(this, destFile)).getCode();

            if (replacement) {
                data.replaceCode(newCode);

            } else {
                ensureExistence();
                data.addCode(newCode);
                data.sortCode();
            }
            setCurrentCode(filename);
            editor.header.repaint();
            if (editor.untitled) { // TODO probably not necessary? problematic?
                // Mark the new code as modified so that the sketch is saved
                current.getCode().setModified(true);
            }

        } else {
            if (editor.untitled) { // TODO probably not necessary? problematic?
                // If a file has been added, mark the main code as modified so
                // that the sketch is properly saved.
                data.getCode(0).setModified(true);
            }
        }
        return true;
    }

    public void importLibrary(UserLibrary lib) throws IOException {
        importLibrary(lib.getSrcFolder());
    }

    /**
     * Add import statements to the current tab for all of packages inside
     * the specified jar file.
     */
    private void importLibrary(File jarPath) throws IOException {
        // make sure the user didn't hide the sketch folder
        ensureExistence();

        String list[] = Base.headerListFromIncludePath(jarPath);
        if (list == null || list.length == 0) {
            return;
        }

        // import statements into the main sketch file (code[0])
        // if the current code is a .java file, insert into current
        //if (current.flavor == PDE) {
        if (hasDefaultExtension(current.getCode())) {
            setCurrentCode(0);
        }
        // could also scan the text in the file to see if each import
        // statement is already in there, but if the user has the import
        // commented out, then this will be a problem.
        StringBuilder buffer = new StringBuilder();
        for (String aList : list) {
            buffer.append("#include <");
            buffer.append(aList);
            buffer.append(">\n");
        }
        buffer.append('\n');
        buffer.append(editor.getText());
        editor.setText(buffer.toString());
        editor.setSelection(0, 0); // scroll to start
        setModified(true);
    }

    /**
     * Change what file is currently being edited. Changes the current tab index.
     * <OL>
     * <LI> store the String for the text of the current file.
     * <LI> retrieve the String for the text of the new file.
     * <LI> change the text that's visible in the text area
     * </OL>
     */
    public void setCurrentCode(int which) {
        setCurrentCode(which, false);
    }

    private void setCurrentCode(int which, boolean forceUpdate) {
        // if current is null, then this is the first setCurrent(0)
        if (!forceUpdate && (currentIndex == which) && (current != null)) {
            return;
        }

        // get the text currently being edited
        if (current != null) {
            current.getCode().setProgram(editor.getText());
            current.setSelectionStart(editor.getSelectionStart());
            current.setSelectionStop(editor.getSelectionStop());
            current.setScrollPosition(editor.getScrollPosition());
        }

        current = (SketchCodeDocument) data.getCode(which).getMetadata();
        currentIndex = which;

        if (SwingUtilities.isEventDispatchThread()) {
            editor.setCode(current);
        } else {
            try {
                SwingUtilities.invokeAndWait(() -> editor.setCode(current));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        editor.header.rebuild();
    }

    /**
     * Internal helper function to set the current tab based on a name.
     * @param findName the file name (not pretty name) to be shown
     */
    protected void setCurrentCode(String findName) {
        for (SketchCode code : data.getCodes()) {
            if (findName.equals(code.getFileName()) || findName.equals(code.getPrettyName())) {
                setCurrentCode(data.indexOfCode(code));
                return;
            }
        }
    }

    /**
     * Preprocess, Compile, and Run the current code.
     * <P>
     * There are three main parts to this process:
     * <PRE>
     *   (0. if not java, then use another 'engine'.. i.e. python)
     *
     *    1. do the p5 language preprocessing
     *       this creates a working .java file in a specific location
     *       better yet, just takes a chunk of java code and returns a
     *       new/better string editor can take care of saving this to a
     *       file location
     *
     *    2. compile the code from that location
     *       catching errors along the way
     *       placing it in a ready classpath, or .. ?
     *
     *    3. run the code
     *       needs to communicate location for window
     *       and maybe setup presentation space as well
     *       run externally if a code folder exists,
     *       or if more than one file is in the project
     *
     *    X. afterwards, some of these steps need a cleanup function
     * </PRE>
     */
    //protected String compile() throws RunnerException {

    /**
     * When running from the editor, take care of preparations before running
     * the build.
     */
    public void prepare() throws IOException {
        // make sure the user didn't hide the sketch folder
        ensureExistence();

        current.getCode().setProgram(editor.getText());

        // TODO record history here
        //current.history.record(program, SketchHistory.RUN);

        // if an external editor is being used, need to grab the
        // latest version of the code from the file.
        if (PreferencesData.getBoolean("editor.external")) {
            // history gets screwed by the open..
            //String historySaved = history.lastRecorded;
            //handleOpen(sketch);
            //history.lastRecorded = historySaved;

            // nuke previous files and settings, just get things loaded
            load(true);
        }

        //    // handle preprocessing the main file's code
        //    return build(tempBuildFolder.getAbsolutePath());
    }

    /**
     * Run the build inside the temporary build folder.
     * @return null if compilation failed, main class name if not
     * @throws RunnerException
     */
    public String build(boolean verbose, boolean save)
            throws RunnerException, PreferencesMapException, IOException {
        return build(BaseNoGui.getBuildFolder(data).getAbsolutePath(), verbose, save);
    }

    /**
     * Preprocess and compile all the code for this sketch.
     *
     * In an advanced program, the returned class name could be different,
     * which is why the className is set based on the return value.
     * A compilation error will burp up a RunnerException.
     *
     * @return null if compilation failed, main class name if not
     */
    private String build(String buildPath, boolean verbose, boolean save)
            throws RunnerException, PreferencesMapException, IOException {
        // run the preprocessor
        editor.status.progressUpdate(20);

        ensureExistence();

        CompilerProgressListener progressListener = editor.status::progressUpdate;

        String pathToSketch = data.getMainFilePath();
        if (isModified()) {
            pathToSketch = saveSketchInTempFolder();
        }

        return new Compiler(pathToSketch, data, buildPath).build(progressListener, save);
    }

    private String saveSketchInTempFolder() throws IOException {
        File tempFolder = FileUtils.createTempFolder("arduino_", DigestUtils.md5Hex(data.getMainFilePath()));
        DeleteFilesOnShutdown.add(tempFolder);
        FileUtils.copy(getFolder(), tempFolder);

        for (SketchCode sc : Stream.of(data.getCodes()).filter(SketchCode::isModified)
                .collect(Collectors.toList())) {
            Files.write(Paths.get(tempFolder.getAbsolutePath(), sc.getFileName()), sc.getProgram().getBytes());
        }

        return Paths.get(tempFolder.getAbsolutePath(), data.getPrimaryFile().getName()).toString();
    }

    protected boolean exportApplet(boolean usingProgrammer) throws Exception {
        return exportApplet(BaseNoGui.getBuildFolder(data).getAbsolutePath(), usingProgrammer);
    }

    /**
     * Handle export to applet.
     */
    private boolean exportApplet(String appletPath, boolean usingProgrammer) throws Exception {

        prepare();

        // build the sketch
        editor.status.progressNotice(tr("Compiling sketch..."));
        String foundName = build(appletPath, false, false);
        // (already reported) error during export, exit this function
        if (foundName == null)
            return false;

        //    // If name != exportSketchName, then that's weirdness
        //    // BUG unfortunately, that can also be a bug in the preproc :(
        //    if (!name.equals(foundName)) {
        //      Base.showWarning("Error during export",
        //                       "Sketch name is " + name + " but the sketch\n" +
        //                       "name in the code was " + foundName, null);
        //      return false;
        //    }

        editor.status.progressNotice(tr("Uploading..."));
        boolean success = upload(appletPath, foundName, usingProgrammer);
        editor.status.progressUpdate(100);
        return success;
    }

    private boolean upload(String buildPath, String suggestedClassName, boolean usingProgrammer) throws Exception {

        Uploader uploader = new UploaderUtils().getUploaderByPreferences(false);

        boolean success = false;
        do {
            if (uploader.requiresAuthorization() && !PreferencesData.has(uploader.getAuthorizationKey())) {
                PasswordAuthorizationDialog dialog = new PasswordAuthorizationDialog(editor,
                        tr("Type board password to upload a new sketch"));
                dialog.setLocationRelativeTo(editor);
                dialog.setVisible(true);

                if (dialog.isCancelled()) {
                    editor.statusNotice(tr("Upload cancelled"));
                    return false;
                }

                PreferencesData.set(uploader.getAuthorizationKey(), dialog.getPassword());
            }

            List<String> warningsAccumulator = new LinkedList<>();
            try {
                success = new UploaderUtils().upload(data, uploader, buildPath, suggestedClassName, usingProgrammer,
                        false, warningsAccumulator);
            } finally {
                if (uploader.requiresAuthorization() && !success) {
                    PreferencesData.remove(uploader.getAuthorizationKey());
                }
            }

            for (String warning : warningsAccumulator) {
                System.out.print(tr("Warning"));
                System.out.print(": ");
                System.out.println(warning);
            }

        } while (uploader.requiresAuthorization() && !success);

        return success;
    }

    /**
     * Make sure the sketch hasn't been moved or deleted by some
     * nefarious user. If they did, try to re-create it and save.
     * Only checks to see if the main folder is still around,
     * but not its contents.
     */
    private void ensureExistence() {
        if (data.getFolder().exists())
            return;

        Base.showWarning(tr("Sketch Disappeared"),
                tr("The sketch folder has disappeared.\n " + "Will attempt to re-save in the same location,\n"
                        + "but anything besides the code will be lost."),
                null);
        try {
            data.getFolder().mkdirs();
            modified = true;

            for (SketchCode code : data.getCodes()) {
                code.save(); // this will force a save
            }
            calcModified();

        } catch (Exception e) {
            Base.showWarning(tr("Could not re-save sketch"),
                    tr("Could not properly re-save the sketch. " + "You may be in trouble at this point,\n"
                            + "and it might be time to copy and paste " + "your code to another text editor."),
                    e);
        }
    }

    /**
     * Returns true if this is a read-only sketch. Used for the
     * examples directory, or when sketches are loaded from read-only
     * volumes or folders without appropriate permissions.
     */
    public boolean isReadOnly(LibraryList libraries, String examplesPath) {
        String apath = data.getFolder().getAbsolutePath();

        Optional<UserLibrary> libraryThatIncludesSketch = libraries.stream()
                .filter(lib -> apath.startsWith(lib.getInstalledFolder().getAbsolutePath())).findFirst();
        if (libraryThatIncludesSketch.isPresent() && !libraryThatIncludesSketch.get().onGoingDevelopment()) {
            return true;
        }

        return sketchIsSystemExample(apath, examplesPath) || sketchFilesAreReadOnly();
    }

    private boolean sketchIsSystemExample(String apath, String examplesPath) {
        return apath.startsWith(examplesPath);
    }

    private boolean sketchFilesAreReadOnly() {
        for (SketchCode code : data.getCodes()) {
            if (code.isModified() && code.fileReadOnly() && code.fileExists()) {
                return true;
            }
        }
        return false;
    }

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

    // Breaking out extension types in order to clean up the code, and make it
    // easier for other environments (like Arduino) to incorporate changes.

    /**
     * True if the specified code has the default file extension.
     */
    private boolean hasDefaultExtension(SketchCode code) {
        return code.isExtension(getDefaultExtension());
    }

    /**
     * True if the specified extension is the default file extension.
     */
    private boolean isDefaultExtension(String what) {
        return what.equals(getDefaultExtension());
    }

    /**
     * Check this extension (no dots, please) against the list of valid
     * extensions.
     */
    private boolean validExtension(String what) {
        return SketchData.EXTENSIONS.contains(what);
    }

    /**
     * Returns the default extension for this editor setup.
     */
    public String getDefaultExtension() {
        return data.getDefaultExtension();
    }

    static private final List<String> hiddenExtensions = Arrays.asList("ino", "pde");

    public List<String> getHiddenExtensions() {
        return hiddenExtensions;
    }

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

    // Additional accessors added in 0136 because of package work.
    // These will also be helpful for tool developers.

    /**
     * Returns the name of this sketch. (The pretty name of the main tab.)
     */
    public String getName() {
        return data.getName();
    }

    /**
     * Returns path to the main .pde file for this sketch.
     */
    public String getMainFilePath() {
        return data.getMainFilePath();
    }

    /**
     * Returns the sketch folder.
     */
    public File getFolder() {
        return data.getFolder();
    }

    /**
     * Create the data folder if it does not exist already. As a convenience,
     * it also returns the data folder, since it's likely about to be used.
     */
    private File prepareDataFolder() {
        if (!data.getDataFolder().exists()) {
            data.getDataFolder().mkdirs();
        }
        return data.getDataFolder();
    }

    /**
     * Create the code folder if it does not exist already. As a convenience,
     * it also returns the code folder, since it's likely about to be used.
     */
    private File prepareCodeFolder() {
        if (!data.getCodeFolder().exists()) {
            data.getCodeFolder().mkdirs();
        }
        return data.getCodeFolder();
    }

    public SketchCode[] getCodes() {
        return data.getCodes();
    }

    public int getCodeCount() {
        return data.getCodeCount();
    }

    public SketchCode getCode(int index) {
        return data.getCode(index);
    }

    public int getCodeIndex(SketchCode who) {
        return data.indexOfCode(who);
    }

    public SketchCode getCurrentCode() {
        return current.getCode();
    }

    private void setUntitled(boolean u) {
        editor.untitled = u;
    }

    public boolean isUntitled() {
        return editor.untitled;
    }

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

    /**
     * Convert to sanitized name and alert the user
     * if changes were made.
     */
    private static String checkName(String origName) {
        String newName = BaseNoGui.sanitizeName(origName);

        if (!newName.equals(origName)) {
            String msg = tr("The sketch name had to be modified. Sketch names can only consist\n"
                    + "of ASCII characters and numbers (but cannot start with a number).\n"
                    + "They should also be less than 64 characters long.");
            System.out.println(msg);
        }
        return newName;
    }

}