Make the change detector not reload the sketch

Fixes #4713, fixes #4849
This commit is contained in:
George Bateman
2017-04-22 15:29:18 +01:00
parent c5d9b713b8
commit 19fc936a7f
4 changed files with 244 additions and 73 deletions
+36 -14
View File
@@ -265,6 +265,34 @@ public class Messages {
"<br>" + secondary, title,
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
} else {
int result = showCustomQuestion(editor, title, primary, secondary,
0, "Yes", "No");
if (result == 0) {
return JOptionPane.YES_OPTION;
} else if (result == 1) {
return JOptionPane.NO_OPTION;
} else {
return JOptionPane.CLOSED_OPTION;
}
}
}
/**
* @param highlight A valid array index for options[] that specifies the
* default (i.e. safe) choice.
* @return The (zero-based) index of the selected value, -1 otherwise.
*/
static public int showCustomQuestion(Frame editor, String title,
String primary, String secondary,
int highlight, String... options) {
Object result;
if (!Platform.isMacOS()) {
return JOptionPane.showOptionDialog(editor,
"<html><body><b>" + primary + "</b><br>" + secondary, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null,
options, options[highlight]);
} else {
// Pane formatting adapted from the Quaqua guide
// http://www.randelshofer.ch/quaqua/guide/joptionpane.html
@@ -275,29 +303,23 @@ public class Messages {
"p { font: 11pt \"Lucida Grande\"; margin-top: 8px; width: 300px }"+
"</style> </head>" +
"<b>" + primary + "</b>" +
"<p>" + secondary + "</p>",
"<p>" + secondary, // + "</p>",
JOptionPane.QUESTION_MESSAGE);
String[] options = new String[] {
"Yes", "No"
};
pane.setOptions(options);
// highlight the safest option ala apple hig
pane.setInitialValue(options[0]);
pane.setInitialValue(options[highlight]);
JDialog dialog = pane.createDialog(editor, null);
dialog.setVisible(true);
Object result = pane.getValue();
if (result == options[0]) {
return JOptionPane.YES_OPTION;
} else if (result == options[1]) {
return JOptionPane.NO_OPTION;
} else {
return JOptionPane.CLOSED_OPTION;
}
result = pane.getValue();
}
for (int i = 0; i < options.length; i++) {
if (result != null && result.equals(options[i])) return i;
}
return -1;
}
@@ -340,4 +362,4 @@ public class Messages {
System.err.println(message);
}
}
}
}
+31 -7
View File
@@ -227,6 +227,20 @@ public class Sketch {
}
/**
* Load a tab that the user added to the sketch or modified with an external
* editor.
*/
public void loadNewTab(String filename, String ext, boolean newAddition) {
if (newAddition) {
insertCode(new SketchCode(new File(folder, filename), ext));
} else {
replaceCode(new SketchCode(new File(folder, filename), ext));
}
sortCode();
}
protected void replaceCode(SketchCode newCode) {
for (int i = 0; i < codeCount; i++) {
if (code[i].getFileName().equals(newCode.getFileName())) {
@@ -685,7 +699,11 @@ public class Sketch {
}
protected void removeCode(SketchCode which) {
/**
* Remove a SketchCode from the list of files without deleting its file.
* @see #handleDeleteCode()
*/
public void removeCode(SketchCode which) {
// remove it from the internal list of files
// resort internal list of files
for (int i = 0; i < codeCount; i++) {
@@ -757,6 +775,16 @@ public class Sketch {
}
/**
* Ensure that all SketchCodes are up-to-date, so that sc.save() works.
*/
public void updateSketchCodes() {
// if (current.isModified()) {
current.setProgram(editor.getText());
// }
}
/**
* Save all code in the current sketch. This just forces the files to save
* in place, so if it's an untitled (un-saved) sketch, saveAs() should be
@@ -767,9 +795,7 @@ public class Sketch {
ensureExistence();
// first get the contents of the editor text area
// if (current.isModified()) {
current.setProgram(editor.getText());
// }
updateSketchCodes();
// don't do anything if not actually modified
//if (!modified) return false;
@@ -911,9 +937,7 @@ public class Sketch {
// 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());
}
updateSketchCodes();
File[] copyItems = folder.listFiles(new FileFilter() {
public boolean accept(File file) {
+165 -52
View File
@@ -3,6 +3,9 @@ package processing.app.ui;
import java.awt.EventQueue;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.nio.file.Files;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -10,13 +13,14 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.swing.JOptionPane;
import processing.app.Language;
import processing.app.Messages;
import processing.app.Platform;
import processing.app.Preferences;
import processing.app.Sketch;
import processing.app.SketchCode;
@@ -28,6 +32,7 @@ public class ChangeDetector implements WindowFocusListener {
private List<String> ignoredAdditions = new ArrayList<>();
private List<SketchCode> ignoredRemovals = new ArrayList<>();
private List<SketchCode> ignoredModifications = new ArrayList<>();
// Windows and others seem to have a few hundred ms difference in reported
// times, so we're arbitrarily setting a gap in time here.
@@ -75,9 +80,9 @@ public class ChangeDetector implements WindowFocusListener {
// Synchronize, we are running async and touching fields
private synchronized void checkFiles() {
List<String> filenames = new ArrayList<>();
sketch.getSketchCodeFiles(filenames, null);
List<String> extensions = new ArrayList<>();
sketch.getSketchCodeFiles(filenames, extensions);
SketchCode[] codes = sketch.getCode();
@@ -97,9 +102,13 @@ public class ChangeDetector implements WindowFocusListener {
.filter(f -> !codeFilenames.contains(f))
.collect(Collectors.toList());
// Show prompt if there are any added files which were not previously ignored
boolean added = addedFilenames.stream()
.anyMatch(f -> !ignoredAdditions.contains(f));
// Added files that are actually candidates for a new tab
List<String> addedTabsFinal = addedFilenames.stream()
.filter(f -> !ignoredAdditions.contains(f))
.collect(Collectors.toList());
// Take action if there are any added files which were not previously ignored
boolean added = !addedTabsFinal.isEmpty();
// REMOVED FILES
@@ -107,10 +116,12 @@ public class ChangeDetector implements WindowFocusListener {
// Get codes which don't have file
List<SketchCode> removedCodes = Optional.ofNullable(existsMap.get(Boolean.FALSE))
.orElse(Collections.emptyList());
List<SketchCode> removedCodesFinal = removedCodes.stream()
.filter(code -> !ignoredRemovals.contains(code))
.collect(Collectors.toList());
// Show prompt if there are any removed codes which were not previously ignored
boolean removed = removedCodes.stream()
.anyMatch(code -> !ignoredRemovals.contains(code));
boolean removed = !removedCodesFinal.isEmpty();
/// MODIFIED FILES
@@ -120,6 +131,7 @@ public class ChangeDetector implements WindowFocusListener {
.orElse(Collections.emptyList())
.stream()
.filter(code -> {
if (ignoredModifications.contains(code)) return false;
long fileLastModified = code.getFile().lastModified();
long codeLastModified = code.getLastModified();
long diff = fileLastModified - codeLastModified;
@@ -130,16 +142,22 @@ public class ChangeDetector implements WindowFocusListener {
// Show prompt if any open codes were modified
boolean modified = !modifiedCodes.isEmpty();
boolean ask = added || removed || modified;
boolean changes = added || removed || modified;
// Do both PDE and disk change for any one file?
List<SketchCode> mergeConflicts = modifiedCodes.stream()
.filter(SketchCode::isModified)
.collect(Collectors.toList());
boolean ask = !mergeConflicts.isEmpty() || removed;
if (DEBUG) {
System.out.println("ask: " + ask + "\n" +
"added filenames: " + addedFilenames + ",\n" +
"ignored added: " + ignoredAdditions + ",\n" +
"removed codes: " + removedCodes + ",\n" +
"ignored removed: " + ignoredRemovals + ",\n" +
"modified codes: " + modifiedCodes);
System.out.println("ask: " + ask + "\n" +
"merge conflicts: " + mergeConflicts + ",\n" +
"added filenames: " + addedFilenames + ",\n" +
"added final: " + addedTabsFinal + ",\n" +
"ignored added: " + ignoredAdditions + ",\n" +
"removed codes: " + removedCodes + ",\n" +
"ignored removed: " + ignoredRemovals + ",\n" +
"modified codes: " + modifiedCodes + "\n");
}
@@ -150,39 +168,111 @@ public class ChangeDetector implements WindowFocusListener {
// Wait for EDT to finish its business
// We need to stay in synchronized scope because of ignore lists
EventQueue.invokeAndWait(() -> {
// Show prompt if something interesting happened
if (ask && showReloadPrompt()) {
// She said yes!!!
if (sketch.getMainFile().exists()) {
sketch.reload();
editor.rebuildHeader();
} else {
// If the main file was deleted, and that's why we're here,
// then we need to re-save the sketch instead.
// Mark everything as modified so that it saves properly
for (SketchCode code : codes) {
code.setModified(true);
// No prompt yet.
if (changes) {
for (int i = 0; i < filenames.size(); i++) {
for (String addedTab : addedTabsFinal) {
if (filenames.get(i).equals(addedTab)) {
sketch.loadNewTab(filenames.get(i), extensions.get(i), true);
break;
}
}
try {
sketch.save();
} catch (Exception e) {
//if that didn't work, tell them it's un-recoverable
Messages.showError("Reload Failed", "The main file for this sketch was deleted\n" +
"and could not be rewritten.", e);
}
for (SketchCode modifiedCode : modifiedCodes) {
if (!mergeConflicts.contains(modifiedCode)) {
sketch.loadNewTab(modifiedCode.getFileName(),
modifiedCode.getExtension(), false);
break;
}
}
// Destructive actions, so prompt.
if (ask) {
sketch.updateSketchCodes();
showReloadPrompt(mergeConflicts, removedCodesFinal,
scReload -> {
try {
File file = scReload.getFile();
File autosave = File.createTempFile(scReload.getPrettyName(),
".autosave", file.getParentFile());
scReload.copyTo(autosave);
} catch (IOException e) {
Messages.showWarning("Could not autosave modified tab",
"Your changes to " + scReload.getPrettyName() +
" have not been saved, so we won't load the new version.", e);
scReload.setModified(true); // So we'll have another go at saving
// it later,
ignoredModifications.add(scReload); // but not create a loop.
return;
}
sketch.loadNewTab(scReload.getFileName(), scReload.getExtension(), false);
},
scKeep -> {
try {
File file = scKeep.getFile();
File autosave = File.createTempFile(scKeep.getPrettyName(),
".autosave", file.getParentFile());
// It is platform-dependent whether File.renameTo would let
// you overwrite the dummy file autosave, which is used to
// make sure all autosave files are unique.
Files.move(file.toPath(), autosave.toPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
Messages.showWarning("Move failed",
"Could not move the external editor's"
+ " version of " + scKeep.getPrettyName() + " to a safe"
+ " location; make a copy of it before saving the sketch"
+ " if you need it.", e);
ignoredModifications.add(scKeep); // No infinite loops.
return;
}
try {
scKeep.save();
} catch (IOException e) {
Messages.showWarning("Save failed",
"Did not save " + scKeep.getPrettyName() + " after"
+ " the file was changed.", e);
ignoredModifications.add(scKeep); // No infinite loops.
}
},
scDelete -> sketch.removeCode(scDelete),
scResave -> {
try {
scResave.save();
} catch (IOException e) {
if (sketch.getCode(0).equals(scResave)) {
// Not a fatal error; the sketch has to stay open if
// they're going to save the code that's in it.
Messages.showWarning(
scResave.getFileName() + " deleted and not re-saved",
"Your main tab was deleted, and Processing couldn't " +
"resave it.\nYour sketch won't work without the " +
"main tab.", e);
} else {
Messages.showWarning("Could not re-save deleted tab",
"Your copy of " + scResave.getPrettyName() +
" will stay in the editor.", e);
}
ignoredRemovals.add(scResave);
scResave.setModified(true); // So we'll have another go at
// saving it later.
}
}
);
}
editor.rebuildHeader();
sketch.handleNextCode();
sketch.handlePrevCode();
editor.repaintHeader();
// Sketch was reloaded, clear ignore lists
ignoredAdditions.clear();
ignoredRemovals.clear();
//ignoredAdditions.clear();
//ignoredRemovals.clear();
return;
}
// Update ignore lists to get rid of old stuff
ignoredAdditions = addedFilenames;
ignoredRemovals = removedCodes;
// If something changed, set modified flags and modification times
if (!removedCodes.isEmpty() || !modifiedCodes.isEmpty()) {
Stream.concat(removedCodes.stream(), modifiedCodes.stream())
@@ -199,21 +289,44 @@ public class ChangeDetector implements WindowFocusListener {
} catch (InvocationTargetException e) {
Messages.loge("exception in ChangeDetector", e);
}
}
/**
* Prompt the user whether to reload the sketch. If the user says yes,
* perform the actual reload.
* @return true if user said yes, false if they hit No or closed the window
* Prompt the user what do do about each tab. Passes the tab to the user's
* choice of Consumer. Won't let you delete the main tab.
*/
private boolean showReloadPrompt() {
int response = Messages
.showYesNoQuestion(editor, "File Modified",
"Your sketch has been modified externally.<br>" +
"Would you like to reload the sketch?",
"If you reload the sketch, any unsaved changes will be lost.");
return response == JOptionPane.YES_OPTION;
private void showReloadPrompt(
List<SketchCode> mergeConflict, List<SketchCode> removed,
Consumer<SketchCode> modifiedReload, Consumer<SketchCode> modifiedKeep,
Consumer<SketchCode> delete, Consumer<SketchCode> deletedResave) {
for (SketchCode sc : mergeConflict) {
if (1 == Messages.showCustomQuestion(editor,
Language.text("change_detect.reload.title"),
Language.interpolate("change_detect.reload.question", sc.getFileName()),
Language.text("change_detect.reload.comment"),
0,
Language.text("change_detect.button.keep"),
Language.text("change_detect.button.load_new"))) {
modifiedReload.accept(sc);
} else {
modifiedKeep.accept(sc);
}
}
for (SketchCode sc : removed) {
if (!sketch.getCode(0).equals(sc) &&
1 == Messages.showCustomQuestion(editor,
Language.text("change_detect.delete.title"),
Language.interpolate("change_detect.delete.question", sc.getFileName()),
Language.text("change_detect.delete.comment"),
0,
Language.text("change_detect.button.resave"),
Language.text("change_detect.button.discard"))) {
delete.accept(sc);
} else {
deletedResave.accept(sc);
}
}
}
}