diff --git a/app/src/processing/app/Messages.java b/app/src/processing/app/Messages.java
index 8f6eaae3e..bcc53b2ad 100644
--- a/app/src/processing/app/Messages.java
+++ b/app/src/processing/app/Messages.java
@@ -265,6 +265,34 @@ public class Messages {
"
" + 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,
+ "
" + primary + "
" + 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 }"+
" " +
"" + primary + "" +
- "" + secondary + "
",
+ "" + secondary, // + "
",
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);
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/processing/app/Sketch.java b/app/src/processing/app/Sketch.java
index 214080f2b..2cd1d7227 100644
--- a/app/src/processing/app/Sketch.java
+++ b/app/src/processing/app/Sketch.java
@@ -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) {
diff --git a/app/src/processing/app/ui/ChangeDetector.java b/app/src/processing/app/ui/ChangeDetector.java
index 6365740e7..c1996633b 100644
--- a/app/src/processing/app/ui/ChangeDetector.java
+++ b/app/src/processing/app/ui/ChangeDetector.java
@@ -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 ignoredAdditions = new ArrayList<>();
private List ignoredRemovals = new ArrayList<>();
+ private List 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 filenames = new ArrayList<>();
- sketch.getSketchCodeFiles(filenames, null);
+ List 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 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 removedCodes = Optional.ofNullable(existsMap.get(Boolean.FALSE))
.orElse(Collections.emptyList());
+ List 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 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.
" +
- "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 mergeConflict, List removed,
+ Consumer modifiedReload, Consumer modifiedKeep,
+ Consumer delete, Consumer 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);
+ }
+ }
}
}
diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties
index f1c3eedb1..c51e740c4 100644
--- a/build/shared/lib/languages/PDE.properties
+++ b/build/shared/lib/languages/PDE.properties
@@ -445,6 +445,18 @@ ensure_exist.messages.unrecoverable.description = Could not properly re-save the
# Check name
check_name.messages.is_name_modified = The sketch name had to be modified. Sketch names can only consist\nof ASCII characters and numbers (but cannot start with a number).\nThey should also be less than 64 characters long.
+# External changes detector
+change_detect.reload.title=Tab modified externally
+change_detect.reload.question="%s" was modified by another program.
+change_detect.reload.comment=Would you like to keep this version or load the new changes?\nEither way, the version you discard will be saved to your sketch folder.
+change_detect.button.keep=Keep
+change_detect.button.load_new=Load changes
+change_detect.delete.title=Tab deleted externally
+change_detect.delete.question="%s" has disappeared from the sketch folder.
+change_detect.delete.comment=Would you like to re-save it or remove it from your sketch?
+change_detect.button.discard=Remove permanently
+change_detect.button.resave=Re-save
+
# ---------------------------------------
# Contributions