mirror of
https://github.com/processing/processing4.git
synced 2026-06-16 04:26:26 +02:00
Make the change detector not reload the sketch
Fixes #4713, fixes #4849
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user