Refactor file adding and renaming, and save as handling

This commits replaces a significant part of the code handling these
features. A lot of responsibilities are moved from SketchController to
Sketch, though the code involved is rewritten mostly.

Most of the handling now happens inside Sketch, including various checks
against the new filename. Basically SketchController processes the user
input to decide what needs to be done, and Sketch checks if it can be
done and does it.

If problems occur, an IOException is thrown, using a translated error
message that is shown by SketchController as-is. This might not be the
best way to transfer error messages (regular IOExceptions might contain
less-friendly messages), so this might need further improvement later.

In addition to moving around code and responsibilities, this code also
changes behaviour in some places:
 - Because Sketch and SketchFile are now in control of renames and
   saves, they can update their internal state after a rename. This
   removes the need for reloading the entire sketch after a rename or
   save as and allows `Editor.handleOpenUnchecked()` to be removed.
 - When renaming the entire sketch, all files used to be saved before
   renaming, since the sketch would be re-opened after renaming. Since
   the re-opening no longer happens, there is no longer a need to save
   the sketch, so any unsaved changes remain unsaved in the editor after
   renaming the sketch.
 - When renaming or adding new files, duplicate filenames are detected.
   Initially, this happened case sensitively, but it was later changed to
   use case insensitive matching to prevent problems on Windows (where
   filenames cannot differ in just case). To prevent complexity, this
   did not distinguish between systems. In commit 5fbf9621f6 (Sketch
   rename: allowig a case change rename if NOT on windows), the
   intention was to only do case insensitive checking on Windows, but it
   effectively disabled all checking on other systems, making the check
   not catch duplicate filenames at all.

   With this commit, all these checks are done using `File.equals()`
   instead of comparing strings, which is already aware of the case
   sensitivity of the platform and should act accordingly.
 - Some error messages were changed.
 - When adding a file, an empty file is not created directly, but only a
   SketchFile and EditorTab is added. When the sketch is saved, the file
   is created.
 - When importing a file that already exists (thus overwriting it),
   instead of replacing the SketchFile instance, this just lets the
   EditorTab reload its contents. This was broken since the introduction
   of EditorTab. The file would be replaced, but not this was not
   reflected in the editor, which is now fixed. This change allows
   `Sketch.replaceFile()` to be removed.
 - When importing a file that does not exist yet (thus adding it), a tab
   is now also added for it (in addition to a SketchFile). This was
   broken since the introduction of EditorTab, and would result in the
   file being added, but not shown in the editor.

This commit adds a `Sketch.renameFileTo()` method, to rename a single
file within the sketch. It would be better to integrate its contents
into `Sketch.renameTo()`, but that does not have access to the `Sketch`
instance it is contained in. This will be changed in a future commit.
This commit is contained in:
Matthijs Kooijman 2015-12-21 17:00:50 +01:00 committed by Martino Facchin
parent 9705e1e734
commit 72f815bcf9
6 changed files with 269 additions and 240 deletions

View File

@ -1642,6 +1642,13 @@ public class Editor extends JFrame implements RunnerListener {
return tabs.get(findTabIndex(file));
}
/**
* Finds the index of the tab showing the given file. Matches the file against
* EditorTab.getSketchFile() using ==.
*
* @returns The index of the tab for the given file, or -1 if no such tab was
* found.
*/
public int findTabIndex(final SketchFile file) {
for (int i = 0; i < tabs.size(); ++i) {
if (tabs.get(i).getSketchFile() == file)
@ -1650,6 +1657,21 @@ public class Editor extends JFrame implements RunnerListener {
return -1;
}
/**
* Finds the index of the tab showing the given file. Matches the file against
* EditorTab.getSketchFile().getFile() using equals.
*
* @returns The index of the tab for the given file, or -1 if no such tab was
* found.
*/
public int findTabIndex(final File file) {
for (int i = 0; i < tabs.size(); ++i) {
if (tabs.get(i).getSketchFile().getFile().equals(file))
return i;
}
return -1;
}
/**
* Create tabs for each of the current sketch's files, removing any existing
* tabs.
@ -1872,23 +1894,6 @@ public class Editor extends JFrame implements RunnerListener {
}
}
/**
* Open a sketch from a particular path, but don't check to save changes.
* Used by Sketch.saveAs() to re-open a sketch after the "Save As"
*/
protected void handleOpenUnchecked(File file, int codeIndex,
int selStart, int selStop, int scrollPos) {
handleOpenInternal(file);
// Replacing a document that may be untitled. If this is an actual
// untitled document, then editor.untitled will be set by Base.
untitled = false;
selectTab(codeIndex);
getCurrentTab().setSelection(selStart, selStop);
getCurrentTab().setScrollPosition(scrollPos);
}
/**
* Second stage of open, occurs after having checked to see if the
* modifications (if any) to the previous sketch need to be saved.

View File

@ -334,7 +334,7 @@ public class EditorTab extends JPanel implements SketchFile.TextStorage {
/**
* Reload the contents of our file.
*/
private void reload() {
public void reload() {
String text;
try {
text = file.load();

View File

@ -133,213 +133,79 @@ public class SketchController {
* where they diverge.
*/
protected void nameCode(String newName) {
SketchFile current = editor.getCurrentTab().getSketchFile();
int currentIndex = editor.getCurrentTabIndex();
// 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 += "." + Sketch.DEFAULT_SKETCH_EXTENSION;
}
// 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())
&& 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) {
if (newName.charAt(0) == '.') {
Base.showWarning(tr("Problem with rename"),
tr("The name cannot start with a period."), null);
return;
}
FileUtils.SplitFile split = FileUtils.splitFilename(newName);
if (split.extension.equals(""))
split.extension = Sketch.DEFAULT_SKETCH_EXTENSION;
if (!Sketch.EXTENSIONS.contains(split.extension)) {
Base.showWarning(tr("Problem with rename"),
I18n.format(tr("\".{0}\" is not a valid extension."),
split.extension),
null);
String msg = I18n.format(tr("\".{0}\" is not a valid extension."),
split.extension);
Base.showWarning(tr("Problem with rename"), msg, null);
return;
}
// Don't let the user create the main tab as a .java file instead of .pde
if (!split.extension.equals(Sketch.DEFAULT_SKETCH_EXTENSION)) {
if (renamingCode) { // If creating a new tab, don't show this error
if (current.isPrimary()) { // 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;
}
}
}
// Sanitize name
String sanitaryName = BaseNoGui.sanitizeName(split.basename);
newName = sanitaryName + "." + split.extension;
// 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 (SketchFile file : sketch.getFiles()) {
if (newName.equalsIgnoreCase(file.getFileName()) && OSUtils.isWindows()) {
Base.showMessage(tr("Error"),
I18n.format(
tr("A file named \"{0}\" already exists in \"{1}\""),
file.getFileName(),
sketch.getFolder().getAbsolutePath()
));
return;
}
}
File newFile = new File(sketch.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;
// }
split.basename = BaseNoGui.sanitizeName(split.basename);
newName = split.join();
if (renamingCode) {
SketchFile current = editor.getCurrentTab().getSketchFile();
if (current.isPrimary()) {
// get the new folder name/location
String folderName = newName.substring(0, newName.indexOf('.'));
File newFolder = new File(sketch.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);
if (!split.extension.equals(Sketch.DEFAULT_SKETCH_EXTENSION)) {
Base.showWarning(tr("Problem with rename"),
tr("The main file cannot use an extension"), 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()) {
try {
// save this new SketchFile
current.save();
} catch (Exception e) {
Base.showWarning(tr("Error"), tr("Could not rename the sketch. (0)"), e);
return;
}
}
if (!current.renameTo(newFile)) {
Base.showWarning(tr("Error"),
I18n.format(
tr("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
// Primary file, rename the entire sketch
final File parent = sketch.getFolder().getParentFile();
File newFolder = new File(parent, split.basename);
try {
for (SketchFile file : sketch.getFiles()) {
file.save();
}
} catch (Exception e) {
Base.showWarning(tr("Error"), tr("Could not rename the sketch. (1)"), e);
sketch.renameTo(newFolder);
} catch (IOException e) {
// This does not pass on e, to prevent showing a backtrace for
// "normal" errors.
Base.showWarning(tr("Error"), e.getMessage(), null);
return;
}
// now rename the sketch folder and re-open
boolean success = sketch.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.getCurrentTab().getSelectionStart(),
editor.getCurrentTab().getSelectionStop(),
editor.getCurrentTab().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)) {
Base.showWarning(tr("Error"),
I18n.format(
tr("Could not rename \"{0}\" to \"{1}\""),
current.getFileName(),
newFile.getName()
), null);
} else {
// Non-primary file, rename just that file
try {
sketch.renameFileTo(current, newName);
} catch (IOException e) {
// This does not pass on e, to prevent showing a backtrace for
// "normal" errors.
Base.showWarning(tr("Error"), e.getMessage(), null);
return;
}
}
} else { // creating a new file
SketchFile 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,
sketch.getFolder().getAbsolutePath()
), e);
return;
}
ensureExistence();
SketchFile file = new SketchFile(newFile, false);
try {
file = sketch.addFile(newName);
editor.addTab(file, "");
} catch (IOException e) {
Base.showWarning(tr("Error"),
I18n.format(
"Failed to open tab for new file"
), e);
// This does not pass on e, to prevent showing a backtrace for
// "normal" errors.
Base.showWarning(tr("Error"), e.getMessage(), null);
return;
}
sketch.addFile(file);
editor.selectTab(editor.findTabIndex(file));
}
@ -558,35 +424,17 @@ public class SketchController {
// 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();
// save the other tabs to their new location
for (SketchFile file : sketch.getFiles()) {
if (file.isPrimary()) continue;
File newFile = new File(newFolder, file.getFileName());
file.saveAs(newFile);
try {
sketch.saveAs(newFolder);
} catch (IOException e) {
// This does not pass on e, to prevent showing a backtrace for "normal"
// errors.
Base.showWarning(tr("Error"), e.getMessage(), null);
}
// re-copy the data folder (this may take a while.. add progress bar?)
if (sketch.getDataFolder().exists()) {
File newDataFolder = new File(newFolder, "data");
FileUtils.copy(sketch.getDataFolder(), newDataFolder);
}
// save the main tab with its new name
File newFile = new File(newFolder, newName + ".ino");
sketch.getFile(0).saveAs(newFile);
editor.handleOpenUnchecked(newFile,
editor.getCurrentTabIndex(),
editor.getCurrentTab().getSelectionStart(),
editor.getCurrentTab().getSelectionStop(),
editor.getCurrentTab().getScrollPosition());
// Name changed, rebuild the sketch menus
//editor.sketchbook.rebuildMenusAsync();
editor.base.rebuildSketchbookMenus();
editor.header.rebuild();
// Make sure that it's not an untitled sketch
setUntitled(false);
@ -719,16 +567,24 @@ public class SketchController {
}
if (!isData) {
SketchFile newFile = new SketchFile(destFile, false);
int tabIndex;
if (replacement) {
sketch.replaceFile(newFile);
tabIndex = editor.findTabIndex(destFile);
editor.getTabs().get(tabIndex).reload();
} else {
ensureExistence();
sketch.addFile(newFile);
SketchFile sketchFile;
try {
sketchFile = sketch.addFile(destFile.getName());
editor.addTab(sketchFile, null);
} catch (IOException e) {
// This does not pass on e, to prevent showing a backtrace for
// "normal" errors.
Base.showWarning(tr("Error"), e.getMessage(), null);
return false;
}
tabIndex = editor.findTabIndex(sketchFile);
}
editor.selectTab(editor.findTabIndex(newFile));
editor.selectTab(tabIndex);
}
return true;
}

View File

@ -159,20 +159,6 @@ public class Sketch {
return getPrimaryFile().getFile().getAbsolutePath();
}
public void addFile(SketchFile file) {
files.add(file);
Collections.sort(files, CODE_DOCS_COMPARATOR);
}
protected void replaceFile(SketchFile newCode) {
for (SketchFile file : files) {
if (file.getFileName().equals(newCode.getFileName())) {
files.set(files.indexOf(file), newCode);
return;
}
}
}
public SketchFile getFile(int i) {
return files.get(i);
}
@ -204,4 +190,164 @@ public class Sketch {
}
return false;
}
/**
* Finds the file with the given filename and returns its index.
* Returns -1 when the file was not found.
*/
public int findFileIndex(File filename) {
int i = 0;
for (SketchFile file : files) {
if (file.getFile().equals(filename))
return i;
i++;
}
return -1;
}
/**
* Check if renaming/saving this sketch to the given folder would
* cause a problem because: 1. The new folder already exists 2.
* Renaming the primary file would cause a conflict with an existing
* file. If so, an IOEXception is thrown. If not, the name of the new
* primary file is returned.
*/
protected File checkNewFoldername(File newFolder) throws IOException {
String newPrimary = FileUtils.addExtension(newFolder.getName(), DEFAULT_SKETCH_EXTENSION);
// Verify the new folder does not exist yet
if (newFolder.exists()) {
String msg = I18n.format(tr("Sorry, the folder \"{0}\" already exists."), newFolder.getAbsoluteFile());
throw new IOException(msg);
}
// If the folder is actually renamed (as opposed to moved somewhere
// else), check for conflicts using the new filename, but the
// existing folder name.
if(newFolder.getName() != folder.getName())
checkNewFilename(new File(folder, newPrimary));
return new File(newFolder, newPrimary);
}
/**
* Check if renaming or adding a file would cause a problem because
* the file already exists in this sketch. If so, an IOEXception is
* thrown.
*
* @param newFile
* The filename of the new file, or the new name for an
* existing file.
*/
protected void checkNewFilename(File newFile) throws IOException {
// Verify that the sketch doesn't have a filem with the new name
// already, other than the current primary (index 0)
if (findFileIndex(newFile) >= 0) {
String msg = I18n.format(tr("The sketch already contains a file named \"{0}\""), newFile.getName());
throw new IOException(msg);
}
}
/**
* Rename this sketch' folder to the given name. Unlike saveAs(), this
* moves the sketch directory, not leaving anything in the old place.
* This operation does not *save* the sketch, so the files on disk are
* moved, but not modified.
*
* @param newFolder
* The new folder name for this sketch. The new primary
* file's name will be derived from this.
*
* @throws IOException
* When a problem occurs. The error message should be
* already translated.
*/
public void renameTo(File newFolder) throws IOException {
// Check intended rename (throws if there is a problem)
File newPrimary = checkNewFoldername(newFolder);
// Rename the sketch folder
if (!getFolder().renameTo(newFolder))
throw new IOException(tr("Failed to rename sketch folder"));
folder = newFolder;
// Tell each file about its new name
for (SketchFile file : files)
file.renamedTo(new File(newFolder, file.getFileName()));
// And finally, rename the primary file
if (!getPrimaryFile().renameTo(newPrimary))
throw new IOException(tr("Failed to rename primary sketch file"));
}
/**
* Rename the given file to get the given name.
*
* @param sketchfile
* The SketchFile to be renamed.
* @param newName
* The new name, including extension, excluding directory
* name.
* @throws IOException
* When a problem occurs, or is expected to occur. The error
* message should be already translated.
*/
public void renameFileTo(SketchFile sketchfile, String newName) throws IOException {
File newFile = new File(folder, newName);
checkNewFilename(newFile);
sketchfile.renameTo(newFile);
}
public SketchFile addFile(String newName) throws IOException {
// Check the name will not cause any conflicts
File newFile = new File(folder, newName);
checkNewFilename(newFile);
// Add a new sketchFile
SketchFile sketchFile = new SketchFile(newFile, false);
files.add(sketchFile);
Collections.sort(files, CODE_DOCS_COMPARATOR);
return sketchFile;
}
/**
* Save this sketch under the new name given. Unlike renameTo(), this
* leaves the existing sketch in place.
*
* @param newFolder
* The new folder name for this sketch. The new primary
* file's name will be derived from this.
*
* @throws IOException
* When a problem occurs. The error message should be
* already translated.
*/
public void saveAs(File newFolder) throws IOException {
// Check intented rename (throws if there is a problem)
File newPrimary = checkNewFoldername(newFolder);
// Create the folder
if (!newFolder.mkdirs()) {
String msg = I18n.format(tr("Could not create directory \"{0}\""), newFolder.getAbsolutePath());
throw new IOException(msg);
}
// Save the files to their new location
for (SketchFile file : files) {
if (file.isPrimary())
file.saveAs(newPrimary);
else
file.saveAs(new File(newFolder, file.getFileName()));
}
folder = newFolder;
// Copy the data folder (this may take a while.. add progress bar?)
if (getDataFolder().exists()) {
File newDataFolder = new File(newFolder, "data");
FileUtils.copy(getDataFolder(), newDataFolder);
}
}
}

View File

@ -154,12 +154,18 @@ public class SketchFile {
protected boolean renameTo(File what) {
boolean success = file.renameTo(what);
if (success) {
file = what;
}
if (success)
renamedTo(what);
return success;
}
/**
* Should be called when this file was renamed and renameTo could not
* be used (e.g. when renaming the entire sketch directory).
*/
protected void renamedTo(File what) {
file = what;
}
/*
* Returns the filename include extension.
@ -264,5 +270,6 @@ public class SketchFile {
return; /* Nothing to do */
BaseNoGui.saveFile(storage.getText(), newFile);
renamedTo(newFile);
}
}

View File

@ -265,6 +265,24 @@ public class FileUtils {
return new File(file.getParentFile(), replaceExtension(file.getName(), extension));
}
/**
* Adds an extension to the given filename. If it already contains
* one, an additional extension is added. If the extension is the
* empty string, the file is returned unmodified.
*/
public static String addExtension(String filename, String extension) {
return extension.equals("") ? filename : (filename + "." + extension);
}
/**
* Adds an extension to the given filename. If it already contains
* one, an additional extension is added. If the extension is the
* empty string, the file is returned unmodified.
*/
public static File addExtension(File file, String extension) {
return new File(file.getParentFile(), addExtension(file.getName(), extension));
}
/**
* The result of a splitFilename call.
*/
@ -278,10 +296,7 @@ public class FileUtils {
public String extension;
public String join() {
if (extension.equals(""))
return basename;
else
return basename + "." + extension;
return addExtension(basename, extension);
}
}