blob: 3f19cd3a3876b31c97628df2e4d012a262f8c643 [file] [log] [blame]
/* -*- 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 processing.app.debug.BasicUploader;
import processing.app.debug.Compiler;
import processing.app.debug.RunnerException;
import processing.app.debug.Sizer;
import processing.app.debug.Uploader;
import processing.app.helpers.PreferencesMap;
import processing.app.preproc.*;
import processing.core.*;
import static processing.app.I18n._;
import java.awt.*;
import java.io.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
/**
* Stores information about files in the current sketch
*/
public class Sketch {
static private File tempBuildFolder;
private Editor editor;
/** main pde file for this sketch. */
private File primaryFile;
/**
* Name of sketch, which is the name of main file
* (without .pde or .java extension)
*/
private String name;
/** true if any of the files have been modified. */
private boolean modified;
/** folder that contains this sketch */
private File folder;
/** data folder location for this sketch (may not exist yet) */
private File dataFolder;
/** code folder location for this sketch (may not exist yet) */
private File codeFolder;
private SketchCode current;
private int currentIndex;
/**
* Number of sketchCode objects (tabs) in the current sketch. Note that this
* will be the same as code.length, because the getCode() method returns
* just the code[] array, rather than a copy of it, or an array that's been
* resized to just the relevant files themselves.
* http://dev.processing.org/bugs/show_bug.cgi?id=940
*/
private int codeCount;
private SketchCode[] code;
/** Class name for the PApplet, as determined by the preprocessor. */
private String appletClassName;
/** Class path determined during build. */
private String classPath;
/**
* This is *not* the "Processing" libraries path, this is the Java libraries
* path, as in java.library.path=BlahBlah, which identifies search paths for
* DLLs or JNILIBs.
*/
private String libraryPath;
/**
* List of library folders.
*/
private ArrayList<File> importedLibraries;
/**
* 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, String path) throws IOException {
this.editor = editor;
primaryFile = new File(path);
// get the name of the sketch by chopping .pde or .java
// off of the main file name
String mainFilename = primaryFile.getName();
int suffixLength = getDefaultExtension().length() + 1;
name = mainFilename.substring(0, mainFilename.length() - suffixLength);
// lib/build must exist when the application is started
// it is added to the CLASSPATH by default, but if it doesn't
// exist when the application is started, then java will remove
// the entry from the CLASSPATH, causing Runner to fail.
//
/*
tempBuildFolder = new File(TEMP_BUILD_PATH);
if (!tempBuildFolder.exists()) {
tempBuildFolder.mkdirs();
Base.showError("Required folder missing",
"A required folder was missing from \n" +
"from your installation of Processing.\n" +
"It has now been replaced, please restart \n" +
"the application to complete the repair.", null);
}
*/
tempBuildFolder = Base.getBuildFolder();
//Base.addBuildFolderToClassPath();
folder = new File(new File(path).getParent());
//System.out.println("sketch dir is " + folder);
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.
*/
protected void load() {
codeFolder = new File(folder, "code");
dataFolder = new File(folder, "data");
// get list of files in the sketch folder
String list[] = folder.list();
// reset these because load() may be called after an
// external editor event. (fix for 0099)
codeCount = 0;
code = new SketchCode[list.length];
String[] extensions = getExtensions();
for (String filename : list) {
// Ignoring the dot prefix files is especially important to avoid files
// with the ._ prefix on Mac OS X. (You'll see this with Mac files on
// non-HFS drives, i.e. a thumb drive formatted FAT32.)
if (filename.startsWith(".")) continue;
// Don't let some wacko name a directory blah.pde or bling.java.
if (new File(folder, filename).isDirectory()) continue;
// figure out the name without any extension
String base = filename;
// now strip off the .pde and .java extensions
for (String extension : extensions) {
if (base.toLowerCase().endsWith("." + extension)) {
base = base.substring(0, base.length() - (extension.length() + 1));
// Don't allow people to use files with invalid names, since on load,
// it would be otherwise possible to sneak in nasty filenames. [0116]
if (Sketch.isSanitaryName(base)) {
code[codeCount++] =
new SketchCode(new File(folder, filename), extension);
}
}
}
}
// Remove any code that wasn't proper
code = (SketchCode[]) PApplet.subset(code, 0, codeCount);
// move the main class to the first tab
// start at 1, if it's at zero, don't bother
for (int i = 1; i < codeCount; i++) {
//if (code[i].file.getName().equals(mainFilename)) {
if (code[i].getFile().equals(primaryFile)) {
SketchCode temp = code[0];
code[0] = code[i];
code[i] = temp;
break;
}
}
// sort the entries at the top
sortCode();
// set the main file to be the current tab
if (editor != null) {
setCurrentCode(0);
}
}
protected void replaceCode(SketchCode newCode) {
for (int i = 0; i < codeCount; i++) {
if (code[i].getFileName().equals(newCode.getFileName())) {
code[i] = newCode;
break;
}
}
}
protected void insertCode(SketchCode newCode) {
// make sure the user didn't hide the sketch folder
ensureExistence();
// add file to the code/codeCount list, resort the list
//if (codeCount == code.length) {
code = (SketchCode[]) PApplet.append(code, newCode);
codeCount++;
//}
//code[codeCount++] = newCode;
}
protected void sortCode() {
// cheap-ass sort of the rest of the files
// it's a dumb, slow sort, but there shouldn't be more than ~5 files
for (int i = 1; i < codeCount; i++) {
int who = i;
for (int j = i + 1; j < codeCount; j++) {
if (code[j].getFileName().compareTo(code[who].getFileName()) < 0) {
who = j; // this guy is earlier in the alphabet
}
}
if (who != i) { // swap with someone if changes made
SketchCode temp = code[who];
code[who] = code[i];
code[i] = temp;
}
}
}
boolean renamingCode;
/**
* Handler for the New Code menu option.
*/
public void handleNewCode() {
// make sure the user didn't hide the sketch folder
ensureExistence();
// if read-only, give an error
if (isReadOnly()) {
// if the files are read-only, need to first do a "save as".
Base.showMessage(_("Sketch is Read-Only"),
_("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(_("Name for new file:"), "");
}
/**
* Handler for the Rename Code menu option.
*/
public void handleRenameCode() {
// make sure the user didn't hide the sketch folder
ensureExistence();
if (currentIndex == 0 && editor.untitled) {
Base.showMessage(_("Sketch is Untitled"),
_("How about saving the sketch first \n" +
"before trying to rename it?"));
return;
}
// if read-only, give an error
if (isReadOnly()) {
// if the files are read-only, need to first do a "save as".
Base.showMessage(_("Sketch is Read-Only"),
_("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.isExtension("ino")) ?
current.getPrettyName() : current.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.getFileName())) {
// 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(_("Problem with rename"),
_("The name cannot start with a period."), null);
return;
}
String newExtension = newName.substring(dot+1).toLowerCase();
if (!validExtension(newExtension)) {
Base.showWarning(_("Problem with rename"),
I18n.format(
_("\".{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 == code[0]) { // If this is the main tab, disallow
Base.showWarning(_("Problem with rename"),
_("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 = Sketch.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 : code) {
if (newName.equalsIgnoreCase(c.getFileName())) {
Base.showMessage(_("Nope"),
I18n.format(
_("A file named \"{0}\" already exists in \"{1}\""),
c.getFileName(),
folder.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(_("Nope"),
_("You can't have a .cpp file with the same name as the sketch."));
return;
}
if (renamingCode && currentIndex == 0) {
for (int i = 1; i < codeCount; i++) {
if (sanitaryName.equalsIgnoreCase(code[i].getPrettyName()) &&
code[i].getExtension().equalsIgnoreCase("cpp")) {
Base.showMessage(_("Nope"),
I18n.format(
_("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(folder, newName);
// if (newFile.exists()) { // yay! users will try anything
// Base.showMessage("Nope",
// "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(folder.getParentFile(), folderName);
if (newFolder.exists()) {
Base.showWarning(_("Cannot Rename"),
I18n.format(
_("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.isModified()) {
current.setProgram(editor.getText());
try {
// save this new SketchCode
current.save();
} catch (Exception e) {
Base.showWarning(_("Error"), _("Could not rename the sketch. (0)"), e);
return;
}
}
if (!current.renameTo(newFile, newExtension)) {
Base.showWarning(_("Error"),
I18n.format(
_("Could not rename \"{0}\" to \"{1}\""),
current.getFileName(),
newFile.getName()
), null);
return;
}
// save each of the other tabs because this is gonna be re-opened
try {
for (int i = 1; i < codeCount; i++) {
code[i].save();
}
} catch (Exception e) {
Base.showWarning(_("Error"), _("Could not rename the sketch. (1)"), e);
return;
}
// now rename the sketch folder and re-open
boolean success = folder.renameTo(newFolder);
if (!success) {
Base.showWarning(_("Error"), _("Could not rename the sketch. (2)"), null);
return;
}
// if successful, set base properties for the sketch
File newMainFile = new File(newFolder, newName + ".ino");
String newMainFilePath = newMainFile.getAbsolutePath();
// 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(newMainFilePath,
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.renameTo(newFile, newExtension)) {
Base.showWarning(_("Error"),
I18n.format(
_("Could not rename \"{0}\" to \"{1}\""),
current.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(_("createNewFile() returned false"));
}
} catch (IOException e) {
Base.showWarning(_("Error"),
I18n.format(
"Could not create the file \"{0}\" in \"{1}\"",
newFile,
folder.getAbsolutePath()
), e);
return;
}
SketchCode newCode = new SketchCode(newFile, newExtension);
//System.out.println("new code is named " + newCode.getPrettyName() + " " + newCode.getFile());
insertCode(newCode);
}
// sort the entries
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() {
// make sure the user didn't hide the sketch folder
ensureExistence();
// if read-only, give an error
if (isReadOnly()) {
// if the files are read-only, need to first do a "save as".
Base.showMessage(_("Sketch is Read-Only"),
_("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 = { _("OK"), _("Cancel") };
String prompt = (currentIndex == 0) ?
_("Are you sure you want to delete this sketch?") :
I18n.format(_("Are you sure you want to delete \"{0}\"?"), current.getPrettyName());
int result = JOptionPane.showOptionDialog(editor,
prompt,
_("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(folder);
// 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.deleteFile()) {
Base.showMessage(_("Couldn't do it"),
I18n.format(_("Could not delete \"{0}\"."), current.getFileName()));
return;
}
// remove code from the list
removeCode(current);
// just set current tab to the main tab
setCurrentCode(0);
// update the tabs
editor.header.repaint();
}
}
}
protected void removeCode(SketchCode which) {
// remove it from the internal list of files
// resort internal list of files
for (int i = 0; i < codeCount; i++) {
if (code[i] == which) {
for (int j = i; j < codeCount-1; j++) {
code[j] = code[j+1];
}
codeCount--;
code = (SketchCode[]) PApplet.shorten(code);
return;
}
}
System.err.println(_("removeCode: internal error.. could not find code"));
}
/**
* Move to the previous tab.
*/
public void handlePrevCode() {
int prev = currentIndex - 1;
if (prev < 0) prev = codeCount-1;
setCurrentCode(prev);
}
/**
* Move to the next tab.
*/
public void handleNextCode() {
setCurrentCode((currentIndex + 1) % codeCount);
}
/**
* 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.setModified(state);
calcModified();
}
protected void calcModified() {
modified = false;
for (int i = 0; i < codeCount; i++) {
if (code[i].isModified()) {
modified = true;
break;
}
}
editor.header.repaint();
if (Base.isMacOS()) {
// http://developer.apple.com/qa/qa2001/qa1146.html
Object modifiedParam = modified ? Boolean.TRUE : Boolean.FALSE;
editor.getRootPane().putClientProperty("windowModified", 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.isModified()) {
current.setProgram(editor.getText());
}
// don't do anything if not actually modified
//if (!modified) return false;
if (isReadOnly()) {
// if the files are read-only, need to first do a "save as".
Base.showMessage(_("Sketch is read-only"),
_("Some files are marked \"read-only\", so you'll\n" +
"need to re-save this sketch to another location."));
// if the user cancels, give up on the save()
if (!saveAs()) return false;
} else {
// rename .pde files to .ino
File mainFile = new File(getMainFilePath());
File mainFolder = mainFile.getParentFile();
File[] pdeFiles = mainFolder.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.toLowerCase().endsWith(".pde");
}
});
if (pdeFiles != null && pdeFiles.length > 0) {
if (Preferences.get("editor.update_extension") == null) {
Object[] options = { _("OK"), _("Cancel") };
int result = JOptionPane.showOptionDialog(editor,
_("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?"),
_(".pde -> .ino"),
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
options,
options[0]);
if (result != JOptionPane.OK_OPTION) return false; // save cancelled
Preferences.setBoolean("editor.update_extension", true);
}
if (Preferences.getBoolean("editor.update_extension")) {
// Do rename of all .pde files to new .ino extension
for (File pdeFile : pdeFiles)
renameCodeToInoExtension(pdeFile);
}
}
}
for (int i = 0; i < codeCount; i++) {
if (code[i].isModified())
code[i].save();
}
calcModified();
return true;
}
protected boolean renameCodeToInoExtension(File pdeFile) {
for (SketchCode c : code) {
if (!c.getFile().equals(pdeFile))
continue;
String pdeName = pdeFile.getPath();
pdeName = pdeName.substring(0, pdeName.length() - 4) + ".ino";
return c.renameTo(new File(pdeName), "ino");
}
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 {
String newParentDir = null;
String newName = null;
/*
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Save sketch folder as...");
if (isReadOnly() || isUntitled()) {
// default to the sketchbook folder
fc.setCurrentDirectory(new File(Preferences.get("sketchbook.path")));
} else {
// default to the parent folder of where this was
fc.setCurrentDirectory(folder.getParentFile());
}
// can't do this, will try to save into itself by default
//fc.setSelectedFile(folder);
int result = fc.showSaveDialog(editor);
if (result == JFileChooser.APPROVE_OPTION) {
File selection = fc.getSelectedFile();
newParentDir = selection.getParent();
newName = selection.getName();
}
*/
// get new name for folder
FileDialog fd = new FileDialog(editor,
_("Save sketch folder as..."),
FileDialog.SAVE);
if (isReadOnly() || isUntitled()) {
// default to the sketchbook folder
fd.setDirectory(Preferences.get("sketchbook.path"));
} else {
// default to the parent folder of where this was
fd.setDirectory(folder.getParent());
}
String oldName = folder.getName();
fd.setFile(oldName);
fd.setVisible(true);
newParentDir = fd.getDirectory();
newName = fd.getFile();
// user canceled selection
if (newName == null) return false;
newName = Sketch.checkName(newName);
File newFolder = new File(newParentDir, newName);
// String newPath = newFolder.getAbsolutePath();
// String oldPath = folder.getAbsolutePath();
// if (newPath.equals(oldPath)) {
// return false; // Can't save a sketch over itself
// }
// 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 < codeCount; i++) {
if (newName.equalsIgnoreCase(code[i].getPrettyName()) &&
code[i].getExtension().equalsIgnoreCase("cpp")) {
Base.showMessage(_("Nope"),
I18n.format(
_("You can't save the sketch as \"{0}\"\n" +
"because the sketch already has a .cpp file with that name."),
newName
));
return false;
}
}
// check if the paths are identical
if (newFolder.equals(folder)) {
// 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 = folder.getCanonicalPath() + File.separator;
if (newPath.indexOf(oldPath) == 0) {
Base.showWarning(_("How very Borges of you"),
_("You cannot save the sketch into a folder\n" +
"inside itself. This would go on forever."), null);
return false;
}
} catch (IOException e) { }
// 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.isModified()) {
current.setProgram(editor.getText());
}
// save the other tabs to their new location
for (int i = 1; i < codeCount; i++) {
File newFile = new File(newFolder, code[i].getFileName());
code[i].saveAs(newFile);
}
// re-copy the data folder (this may take a while.. add progress bar?)
if (dataFolder.exists()) {
File newDataFolder = new File(newFolder, "data");
Base.copyDir(dataFolder, newDataFolder);
}
// re-copy the code folder
if (codeFolder.exists()) {
File newCodeFolder = new File(newFolder, "code");
Base.copyDir(codeFolder, newCodeFolder);
}
// copy custom applet.html file if one exists
// http://dev.processing.org/bugs/show_bug.cgi?id=485
File customHtml = new File(folder, "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");
code[0].saveAs(newFile);
editor.handleOpenUnchecked(newFile.getPath(),
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()) {
// if the files are read-only, need to first do a "save as".
Base.showMessage(_("Sketch is Read-Only"),
_("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
String prompt =
_("Select an image or other data file to copy to your sketch");
//FileDialog fd = new FileDialog(new Frame(), prompt, FileDialog.LOAD);
FileDialog fd = new FileDialog(editor, prompt, 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(_("One file added to the sketch."));
}
}
/**
* 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(codeFolder, filename);
} else {
for (String extension : getExtensions()) {
String lower = filename.toLowerCase();
if (lower.endsWith("." + extension)) {
destFile = new File(this.folder, filename);
codeExtension = extension;
}
}
if (codeExtension == null) {
prepareDataFolder();
destFile = new File(dataFolder, filename);
}
}
// check whether this file already exists
if (destFile.exists()) {
Object[] options = { _("OK"), _("Cancel") };
String prompt = I18n.format(_("Replace the existing version of {0}?"), filename);
int result = JOptionPane.showOptionDialog(editor,
prompt,
_("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(_("Error adding file"),
I18n.format(_("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(_("You can't fool me"),
_("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(_("Error adding file"),
I18n.format(_("Could not add ''{0}'' to the sketch."), filename),
e);
return false;
}
}
if (codeExtension != null) {
SketchCode newCode = new SketchCode(destFile, codeExtension);
if (replacement) {
replaceCode(newCode);
} else {
insertCode(newCode);
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.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.
code[0].setModified(true);
}
}
return true;
}
/**
* Add import statements to the current tab for all of packages inside
* the specified jar file.
*/
public void importLibrary(String jarPath) {
// make sure the user didn't hide the sketch folder
ensureExistence();
String list[] = Base.headerListFromIncludePath(new File(jarPath));
// 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)) {
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.
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < list.length; i++) {
buffer.append("#include <");
buffer.append(list[i]);
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) {
// if current is null, then this is the first setCurrent(0)
if ((currentIndex == which) && (current != null)) {
return;
}
// get the text currently being edited
if (current != null) {
current.setState(editor.getText(),
editor.getSelectionStart(),
editor.getSelectionStop(),
editor.getScrollPosition());
}
current = code[which];
currentIndex = which;
editor.setCode(current);
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 (int i = 0; i < codeCount; i++) {
if (findName.equals(code[i].getFileName()) ||
findName.equals(code[i].getPrettyName())) {
setCurrentCode(i);
return;
}
}
}
/**
* Cleanup temporary files used during a build/run.
*/
protected void cleanup() {
// if the java runtime is holding onto any files in the build dir, we
// won't be able to delete them, so we need to force a gc here
System.gc();
if (deleteFilesOnNextBuild) {
// delete the entire directory and all contents
// when we know something changed and all objects
// need to be recompiled, or if the board does not
// use setting build.dependency
//Base.removeDir(tempBuildFolder);
// note that we can't remove the builddir itself, otherwise
// the next time we start up, internal runs using Runner won't
// work because the build dir won't exist at startup, so the classloader
// will ignore the fact that that dir is in the CLASSPATH in run.sh
Base.removeDescendants(tempBuildFolder);
deleteFilesOnNextBuild = false;
} else {
// delete only stale source files, from the previously
// compiled sketch. This allows multiple windows to be
// used. Keep everything else, which might be reusable
if (tempBuildFolder.exists()) {
String files[] = tempBuildFolder.list();
for (String file : files) {
if (file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".s")) {
File deleteMe = new File(tempBuildFolder, file);
if (!deleteMe.delete()) {
System.err.println("Could not delete " + deleteMe);
}
}
}
}
}
// Create a fresh applet folder (needed before preproc is run below)
//tempBuildFolder.mkdirs();
}
/**
* 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 {
// called when any setting changes that requires all files to be recompiled
public static void buildSettingChanged() {
deleteFilesOnNextBuild = true;
}
private static boolean deleteFilesOnNextBuild = true;
/**
* When running from the editor, take care of preparations before running
* the build.
*/
public void prepare() {
// make sure the user didn't hide the sketch folder
ensureExistence();
current.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 (Preferences.getBoolean("editor.external")) {
// history gets screwed by the open..
//String historySaved = history.lastRecorded;
//handleOpen(sketch);
//history.lastRecorded = historySaved;
// set current to null so that the tab gets updated
// http://dev.processing.org/bugs/show_bug.cgi?id=515
current = null;
// nuke previous files and settings, just get things loaded
load();
}
// in case there were any boogers left behind
// do this here instead of after exiting, since the exit
// can happen so many different ways.. and this will be
// better connected to the dataFolder stuff below.
cleanup();
// // handle preprocessing the main file's code
// return build(tempBuildFolder.getAbsolutePath());
}
/**
* Build 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.
*
* Setting purty to 'true' will cause exception line numbers to be incorrect.
* Unless you know the code compiles, you should first run the preprocessor
* with purty set to false to make sure there are no errors, then once
* successful, re-export with purty set to true.
*
* @param buildPath Location to copy all the .java files
* @return null if compilation failed, main class name if not
*/
public String preprocess(String buildPath) throws RunnerException {
return preprocess(buildPath, new PdePreprocessor());
}
public String preprocess(String buildPath, PdePreprocessor preprocessor) throws RunnerException {
// make sure the user didn't hide the sketch folder
ensureExistence();
String[] codeFolderPackages = null;
classPath = buildPath;
// // figure out the contents of the code folder to see if there
// // are files that need to be added to the imports
// if (codeFolder.exists()) {
// libraryPath = codeFolder.getAbsolutePath();
//
// // get a list of .jar files in the "code" folder
// // (class files in subfolders should also be picked up)
// String codeFolderClassPath =
// Compiler.contentsToClassPath(codeFolder);
// // append the jar files in the code folder to the class path
// classPath += File.pathSeparator + codeFolderClassPath;
// // get list of packages found in those jars
// codeFolderPackages =
// Compiler.packageListFromClassPath(codeFolderClassPath);
//
// } else {
// libraryPath = "";
// }
// 1. concatenate all .pde files to the 'main' pde
// store line number for starting point of each code bit
StringBuffer bigCode = new StringBuffer();
int bigCount = 0;
for (SketchCode sc : code) {
if (sc.isExtension("ino") || sc.isExtension("pde")) {
sc.setPreprocOffset(bigCount);
bigCode.append(sc.getProgram());
bigCode.append('\n');
bigCount += sc.getLineCount();
}
}
// Note that the headerOffset isn't applied until compile and run, because
// it only applies to the code after it's been written to the .java file.
int headerOffset = 0;
//PdePreprocessor preprocessor = new PdePreprocessor();
try {
headerOffset = preprocessor.writePrefix(bigCode.toString(),
buildPath,
name,
codeFolderPackages);
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
String msg = _("Build folder disappeared or could not be written");
throw new RunnerException(msg);
}
// 2. run preproc on that code using the sugg class name
// to create a single .java file and write to buildpath
String primaryClassName = null;
try {
// if (i != 0) preproc will fail if a pde file is not
// java mode, since that's required
String className = preprocessor.write();
if (className == null) {
throw new RunnerException(_("Could not find main class"));
// this situation might be perfectly fine,
// (i.e. if the file is empty)
//System.out.println("No class found in " + code[i].name);
//System.out.println("(any code in that file will be ignored)");
//System.out.println();
// } else {
// code[0].setPreprocName(className + ".java");
}
// store this for the compiler and the runtime
primaryClassName = className + ".cpp";
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
String msg = _("Build folder disappeared or could not be written");
throw new RunnerException(msg);
} catch (RunnerException pe) {
// RunnerExceptions are caught here and re-thrown, so that they don't
// get lost in the more general "Exception" handler below.
throw pe;
} catch (Exception ex) {
// TODO better method for handling this?
System.err.println(I18n.format(_("Uncaught exception type: {0}"), ex.getClass()));
ex.printStackTrace();
throw new RunnerException(ex.toString());
}
// grab the imports from the code just preproc'd
importedLibraries = new ArrayList<File>();
//Remember to clear library path before building it.
libraryPath = "";
for (String item : preprocessor.getExtraImports()) {
File libFolder = (File) Base.importToLibraryTable.get(item);
//If needed can Debug libraryPath here
if (libFolder != null && !importedLibraries.contains(libFolder)) {
importedLibraries.add(libFolder);
//classPath += Compiler.contentsToClassPath(libFolder);
libraryPath += File.pathSeparator + libFolder.getAbsolutePath();
}
}
// 3. then loop over the code[] and save each .java file
for (SketchCode sc : code) {
if (sc.isExtension("c") || sc.isExtension("cpp") || sc.isExtension("h")) {
// no pre-processing services necessary for java files
// just write the the contents of 'program' to a .java file
// into the build directory. uses byte stream and reader/writer
// shtuff so that unicode bunk is properly handled
String filename = sc.getFileName(); //code[i].name + ".java";
try {
Base.saveFile(sc.getProgram(), new File(buildPath, filename));
} catch (IOException e) {
e.printStackTrace();
throw new RunnerException(I18n.format(_("Problem moving {0} to the build folder"), filename));
}
// sc.setPreprocName(filename);
} else if (sc.isExtension("ino") || sc.isExtension("pde")) {
// The compiler and runner will need this to have a proper offset
sc.addPreprocOffset(headerOffset);
}
}
return primaryClassName;
}
public ArrayList<File> getImportedLibraries() {
return importedLibraries;
}
/**
* Map an error from a set of processed .java files back to its location
* in the actual sketch.
* @param message The error message.
* @param filename The .java file where the exception was found.
* @param line Line number of the .java file for the exception (1-indexed)
* @return A RunnerException to be sent to the editor, or null if it wasn't
* possible to place the exception to the sketch code.
*/
// public RunnerException placeExceptionAlt(String message,
// String filename, int line) {
// String appletJavaFile = appletClassName + ".java";
// SketchCode errorCode = null;
// if (filename.equals(appletJavaFile)) {
// for (SketchCode code : getCode()) {
// if (code.isExtension("ino")) {
// if (line >= code.getPreprocOffset()) {
// errorCode = code;
// }
// }
// }
// } else {
// for (SketchCode code : getCode()) {
// if (code.isExtension("java")) {
// if (filename.equals(code.getFileName())) {
// errorCode = code;
// }
// }
// }
// }
// int codeIndex = getCodeIndex(errorCode);
//
// if (codeIndex != -1) {
// //System.out.println("got line num " + lineNumber);
// // in case this was a tab that got embedded into the main .java
// line -= getCode(codeIndex).getPreprocOffset();
//
// // lineNumber is 1-indexed, but editor wants zero-indexed
// line--;
//
// // getMessage() will be what's shown in the editor
// RunnerException exception =
// new RunnerException(message, codeIndex, line, -1);
// exception.hideStackTrace();
// return exception;
// }
// return null;
// }
/**
* Map an error from a set of processed .java files back to its location
* in the actual sketch.
* @param message The error message.
* @param filename The .java file where the exception was found.
* @param line Line number of the .java file for the exception (0-indexed!)
* @return A RunnerException to be sent to the editor, or null if it wasn't
* possible to place the exception to the sketch code.
*/
public RunnerException placeException(String message,
String dotJavaFilename,
int dotJavaLine) {
int codeIndex = 0; //-1;
int codeLine = -1;
// System.out.println("placing " + dotJavaFilename + " " + dotJavaLine);
// System.out.println("code count is " + getCodeCount());
// first check to see if it's a .java file
for (int i = 0; i < getCodeCount(); i++) {
SketchCode code = getCode(i);
if (!code.isExtension(getDefaultExtension())) {
if (dotJavaFilename.equals(code.getFileName())) {
codeIndex = i;
codeLine = dotJavaLine;
return new RunnerException(message, codeIndex, codeLine);
}
}
}
// If not the preprocessed file at this point, then need to get out
if (!dotJavaFilename.equals(name + ".cpp")) {
return null;
}
// if it's not a .java file, codeIndex will still be 0
// this section searches through the list of .pde files
codeIndex = 0;
for (int i = 0; i < getCodeCount(); i++) {
SketchCode code = getCode(i);
if (code.isExtension(getDefaultExtension())) {
// System.out.println("preproc offset is " + code.getPreprocOffset());
// System.out.println("looking for line " + dotJavaLine);
if (code.getPreprocOffset() <= dotJavaLine) {
codeIndex = i;
// System.out.println("i'm thinkin file " + i);
codeLine = dotJavaLine - code.getPreprocOffset();
}
}
}
// could not find a proper line number, so deal with this differently.
// but if it was in fact the .java file we're looking for, though,
// send the error message through.
// this is necessary because 'import' statements will be at a line
// that has a lower number than the preproc offset, for instance.
// if (codeLine == -1 && !dotJavaFilename.equals(name + ".java")) {
// return null;
// }
return new RunnerException(message, codeIndex, codeLine);
}
/**
* 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) throws RunnerException {
return build(tempBuildFolder.getAbsolutePath(), verbose);
}
/**
* 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
*/
public String build(String buildPath, boolean verbose) throws RunnerException {
// run the preprocessor
editor.status.progressUpdate(20);
String primaryClassName = preprocess(buildPath);
// compile the program. errors will happen as a RunnerException
// that will bubble up to whomever called build().
Compiler compiler = new Compiler();
if (compiler.compile(this, buildPath, primaryClassName, verbose)) {
size(compiler.getBuildPreferences());
return primaryClassName;
}
return null;
}
protected boolean exportApplet(boolean usingProgrammer) throws Exception {
return exportApplet(tempBuildFolder.getAbsolutePath(), usingProgrammer);
}
/**
* Handle export to applet.
*/
public boolean exportApplet(String appletPath, boolean usingProgrammer)
throws RunnerException, IOException, SerialException {
prepare();
// build the sketch
editor.status.progressNotice(_("Compiling sketch..."));
String foundName = build(appletPath, 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(_("Uploading..."));
upload(appletPath, foundName, usingProgrammer);
editor.status.progressUpdate(100);
return true;
}
public void setCompilingProgress(int percent) {
editor.status.progressUpdate(percent);
}
protected void size(PreferencesMap prefs) throws RunnerException {
long size = 0;
String maxsizeString = prefs.get("upload.maximum_size");
if (maxsizeString == null)
return;
long maxsize = Integer.parseInt(maxsizeString);
Sizer sizer = new Sizer(prefs);
try {
size = sizer.computeSize();
System.out.println(I18n
.format(_("Binary sketch size: {0} bytes (of a {1} byte maximum)"),
size, maxsize));
} catch (RunnerException e) {
System.err.println(I18n.format(_("Couldn't determine program size: {0}"),
e.getMessage()));
}
if (size > maxsize)
throw new RunnerException(
_("Sketch too big; see http://www.arduino.cc/en/Guide/Troubleshooting#size for tips on reducing it."));
}
protected String upload(String buildPath, String suggestedClassName, boolean usingProgrammer)
throws RunnerException, SerialException {
Uploader uploader;
// download the program
//
uploader = new BasicUploader();
boolean success = uploader.uploadUsingPreferences(buildPath,
suggestedClassName,
usingProgrammer);
return success ? suggestedClassName : null;
}
/**
* Replace all commented portions of a given String as spaces.
* Utility function used here and in the preprocessor.
*/
static public String scrubComments(String what) {
char p[] = what.toCharArray();
int index = 0;
while (index < p.length) {
// for any double slash comments, ignore until the end of the line
if ((p[index] == '/') &&
(index < p.length - 1) &&
(p[index+1] == '/')) {
p[index++] = ' ';
p[index++] = ' ';
while ((index < p.length) &&
(p[index] != '\n')) {
p[index++] = ' ';
}
// check to see if this is the start of a new multiline comment.
// if it is, then make sure it's actually terminated somewhere.
} else if ((p[index] == '/') &&
(index < p.length - 1) &&
(p[index+1] == '*')) {
p[index++] = ' ';
p[index++] = ' ';
boolean endOfRainbow = false;
while (index < p.length - 1) {
if ((p[index] == '*') && (p[index+1] == '/')) {
p[index++] = ' ';
p[index++] = ' ';
endOfRainbow = true;
break;
} else {
// continue blanking this area
p[index++] = ' ';
}
}
if (!endOfRainbow) {
throw new RuntimeException(_("Missing the */ from the end of a " +
"/* comment */"));
}
} else { // any old character, move along
index++;
}
}
return new String(p);
}
public boolean exportApplicationPrompt() throws IOException, RunnerException {
return false;
}
/**
* Export to application via GUI.
*/
protected boolean exportApplication() throws IOException, RunnerException {
return false;
}
/**
* Export to application without GUI.
*/
public boolean exportApplication(String destPath,
int exportPlatform) throws IOException, RunnerException {
return false;
}
/**
* 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.
*/
protected void ensureExistence() {
if (folder.exists()) return;
Base.showWarning(_("Sketch Disappeared"),
_("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 {
folder.mkdirs();
modified = true;
for (int i = 0; i < codeCount; i++) {
code[i].save(); // this will force a save
}
calcModified();
} catch (Exception e) {
Base.showWarning(_("Could not re-save sketch"),
_("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() {
String apath = folder.getAbsolutePath();
for (File folder : Base.getLibrariesPath()) {
if (apath.startsWith(folder.getAbsolutePath()))
return true;
}
if (apath.startsWith(Base.getExamplesPath()) ||
apath.startsWith(Base.getSketchbookLibrariesPath())) {
return true;
}
// canWrite() doesn't work on directories
// } else if (!folder.canWrite()) {
// check to see if each modified code file can be written to
for (int i = 0; i < codeCount; i++) {
if (code[i].isModified() && code[i].fileReadOnly() &&
code[i].fileExists()) {
// System.err.println("found a read-only file " + code[i].file);
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 extension should be hidden when shown on a tab.
* For Processing, this is true for .pde files. (Broken out for subclasses.)
*/
public boolean hideExtension(String what) {
return getHiddenExtensions().contains(what);
}
/**
* True if the specified code has the default file extension.
*/
public boolean hasDefaultExtension(SketchCode code) {
return code.getExtension().equals(getDefaultExtension());
}
/**
* True if the specified extension is the default file extension.
*/
public boolean isDefaultExtension(String what) {
return what.equals(getDefaultExtension());
}
/**
* Check this extension (no dots, please) against the list of valid
* extensions.
*/
public boolean validExtension(String what) {
String[] ext = getExtensions();
for (int i = 0; i < ext.length; i++) {
if (ext[i].equals(what)) return true;
}
return false;
}
/**
* Returns the default extension for this editor setup.
*/
public String getDefaultExtension() {
return "ino";
}
static private List<String> hiddenExtensions = Arrays.asList("ino", "pde");
public List<String> getHiddenExtensions() {
return hiddenExtensions;
}
/**
* Returns a String[] array of proper extensions.
*/
public String[] getExtensions() {
return new String[] { "ino", "pde", "c", "cpp", "h" };
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
// 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 name;
}
/**
* Returns a file object for the primary .pde of this sketch.
*/
public File getPrimaryFile() {
return primaryFile;
}
/**
* Returns path to the main .pde file for this sketch.
*/
public String getMainFilePath() {
return primaryFile.getAbsolutePath();
//return code[0].file.getAbsolutePath();
}
/**
* Returns the sketch folder.
*/
public File getFolder() {
return folder;
}
/**
* Returns the location of the sketch's data folder. (It may not exist yet.)
*/
public File getDataFolder() {
return dataFolder;
}
/**
* 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.
*/
public File prepareDataFolder() {
if (!dataFolder.exists()) {
dataFolder.mkdirs();
}
return dataFolder;
}
/**
* Returns the location of the sketch's code folder. (It may not exist yet.)
*/
public File getCodeFolder() {
return codeFolder;
}
/**
* 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.
*/
public File prepareCodeFolder() {
if (!codeFolder.exists()) {
codeFolder.mkdirs();
}
return codeFolder;
}
public String getClassPath() {
return classPath;
}
public String getLibraryPath() {
return libraryPath;
}
public SketchCode[] getCode() {
return code;
}
public int getCodeCount() {
return codeCount;
}
public SketchCode getCode(int index) {
return code[index];
}
public int getCodeIndex(SketchCode who) {
for (int i = 0; i < codeCount; i++) {
if (who == code[i]) {
return i;
}
}
return -1;
}
public SketchCode getCurrentCode() {
return current;
}
public void setUntitled(boolean u) {
editor.untitled = u;
}
public boolean isUntitled() {
return editor.untitled;
}
public String getAppletClassName2() {
return appletClassName;
}
// .................................................................
/**
* Convert to sanitized name and alert the user
* if changes were made.
*/
static public String checkName(String origName) {
String newName = sanitizeName(origName);
if (!newName.equals(origName)) {
String msg =
_("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 less than 64 characters long.");
System.out.println(msg);
}
return newName;
}
/**
* Return true if the name is valid for a Processing sketch.
*/
static public boolean isSanitaryName(String name) {
return sanitizeName(name).equals(name);
}
/**
* Produce a sanitized name that fits our standards for likely to work.
* <p/>
* Java classes have a wider range of names that are technically allowed
* (supposedly any Unicode name) than what we support. The reason for
* going more narrow is to avoid situations with text encodings and
* converting during the process of moving files between operating
* systems, i.e. uploading from a Windows machine to a Linux server,
* or reading a FAT32 partition in OS X and using a thumb drive.
* <p/>
* This helper function replaces everything but A-Z, a-z, and 0-9 with
* underscores. Also disallows starting the sketch name with a digit.
*/
static public String sanitizeName(String origName) {
char c[] = origName.toCharArray();
StringBuffer buffer = new StringBuffer();
// can't lead with a digit, so start with an underscore
if ((c[0] >= '0') && (c[0] <= '9')) {
buffer.append('_');
}
for (int i = 0; i < c.length; i++) {
if (((c[i] >= '0') && (c[i] <= '9')) ||
((c[i] >= 'a') && (c[i] <= 'z')) ||
((c[i] >= 'A') && (c[i] <= 'Z'))) {
buffer.append(c[i]);
} else {
buffer.append('_');
}
}
// let's not be ridiculous about the length of filenames.
// in fact, Mac OS 9 can handle 255 chars, though it can't really
// deal with filenames longer than 31 chars in the Finder.
// but limiting to that for sketches would mean setting the
// upper-bound on the character limit here to 25 characters
// (to handle the base name + ".class")
if (buffer.length() > 63) {
buffer.setLength(63);
}
return buffer.toString();
}
}