package processing.app.ui;
import java.awt.EventQueue;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.swing.JOptionPane;
import processing.app.Messages;
import processing.app.Preferences;
import processing.app.Sketch;
import processing.app.SketchCode;
public class ChangeDetector implements WindowFocusListener {
private final Sketch sketch;
private final Editor editor;
private List<String> ignoredAdditions = new ArrayList<>();
private List<SketchCode> ignoredRemovals = 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.
// Mac OS X has an (exactly) one second difference. Not sure if it's a Java
// bug or something else about how OS X is writing files.
static private final int MODIFICATION_WINDOW_MILLIS =
Preferences.getInteger("editor.watcher.window");
// Debugging this feature is particularly difficult, adding an option for it
static private final boolean DEBUG =
Preferences.getBoolean("editor.watcher.debug");
public ChangeDetector(Editor editor) {
this.sketch = editor.sketch;
this.editor = editor;
}
@Override
public void windowGainedFocus(WindowEvent e) {
if (Preferences.getBoolean("editor.watcher")) {
if (sketch != null) {
// make sure the sketch folder exists at all.
// if it does not, it will be re-saved, and no changes will be detected
sketch.ensureExistence(); // <- touches UI, stay on EDT
// TODO: Not sure if we even need to run this async. Usually takes
// just a few ms and we probably want to prevent any changes from
// users until the external changes are sorted out. [jv 2016-12-05]
// Run task in common pool, starting threads directly is so Java 6
ForkJoinPool.commonPool().execute(this::checkFiles);
}
}
}
@Override
public void windowLostFocus(WindowEvent e) {
// Shouldn't need to do anything here, and not storing anything here b/c we
// don't want to assume a loss of focus is required before change detection
}
// Synchronize, we are running async and touching fields
private synchronized void checkFiles() {
List<String> filenames = new ArrayList<>();
sketch.getSketchCodeFiles(filenames, null);
SketchCode[] codes = sketch.getCode();
// Separate codes with and without files
Map<Boolean, List<SketchCode>> existsMap = Arrays.stream(codes)
.collect(Collectors.groupingBy(code -> filenames.contains(code.getFileName())));
// ADDED FILES
List<String> codeFilenames = Arrays.stream(codes)
.map(SketchCode::getFileName)
.collect(Collectors.toList());
// Get filenames which are in filesystem but don't have code
List<String> addedFilenames = filenames.stream()
.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));
// REMOVED FILES
// Get codes which don't have file
List<SketchCode> removedCodes = Optional.ofNullable(existsMap.get(Boolean.FALSE))
.orElse(Collections.emptyList());
// Show prompt if there are any removed codes which were not previously ignored
boolean removed = removedCodes.stream()
.anyMatch(code -> !ignoredRemovals.contains(code));
/// MODIFIED FILES
// Get codes which have file with different modification time
List<SketchCode> modifiedCodes = Optional.ofNullable(existsMap.get(Boolean.TRUE))
.orElse(Collections.emptyList())
.stream()
.filter(code -> {
long fileLastModified = code.getFile().lastModified();
long codeLastModified = code.getLastModified();
long diff = fileLastModified - codeLastModified;
return fileLastModified == 0L || diff > MODIFICATION_WINDOW_MILLIS;
})
.collect(Collectors.toList());
// Show prompt if any open codes were modified
boolean modified = !modifiedCodes.isEmpty();
boolean ask = added || removed || modified;
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);
}
// This has to happen in one go and also touches UI everywhere. It has to
// run on EDT, otherwise windowGainedFocus callback runs again right after
// dismissing the prompt and we get another prompt before we even finished.
try {
// 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);
}
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);
}
}
// Sketch was reloaded, clear ignore lists
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())
.forEach(code -> {
code.setModified(true);
code.setLastModified();
});
// Not sure if this is needed
editor.rebuildHeader();
}
});
} catch (InterruptedException ignore) {
} 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
*/
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;
}
}