From f5ca945b5e96344dbd914309c9fa6dc040c681b6 Mon Sep 17 00:00:00 2001 From: Jakub Valtar Date: Mon, 5 Dec 2016 03:46:44 +0100 Subject: [PATCH] Clean up ChangeDetector Fixes #3965 --- app/src/processing/app/ui/ChangeDetector.java | 337 +++++++----------- 1 file changed, 138 insertions(+), 199 deletions(-) diff --git a/app/src/processing/app/ui/ChangeDetector.java b/app/src/processing/app/ui/ChangeDetector.java index 866d2ffc4..6365740e7 100644 --- a/app/src/processing/app/ui/ChangeDetector.java +++ b/app/src/processing/app/ui/ChangeDetector.java @@ -1,13 +1,18 @@ package processing.app.ui; import java.awt.EventQueue; -import java.awt.Frame; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; -import java.io.File; 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; @@ -21,6 +26,9 @@ public class ChangeDetector implements WindowFocusListener { private final Sketch sketch; private final Editor editor; + private List ignoredAdditions = new ArrayList<>(); + private List 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 @@ -32,9 +40,6 @@ public class ChangeDetector implements WindowFocusListener { static private final boolean DEBUG = Preferences.getBoolean("editor.watcher.debug"); - // Store the known number of files to avoid re-asking about the same change -// private int lastKnownCount = -1; - public ChangeDetector(Editor editor) { this.sketch = editor.sketch; @@ -44,27 +49,19 @@ public class ChangeDetector implements WindowFocusListener { @Override public void windowGainedFocus(WindowEvent e) { - // When the window is activated, fire off a Thread to check for changes if (Preferences.getBoolean("editor.watcher")) { - new Thread(new Runnable() { - @Override - public void run() { - 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(); + 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 -// if (lastKnownCount == -1) { -// lastKnownCount = sketch.getCodeCount(); -// } + // 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] - boolean alreadyPrompted = checkFileCount(); - if (!alreadyPrompted) { - checkFileTimes(); - } - } - } - }).start(); + // Run task in common pool, starting threads directly is so Java 6 + ForkJoinPool.commonPool().execute(this::checkFiles); + } } } @@ -76,135 +73,133 @@ public class ChangeDetector implements WindowFocusListener { } - private boolean checkFileCount() { - // check file count first + // Synchronize, we are running async and touching fields + private synchronized void checkFiles() { List filenames = new ArrayList<>(); - sketch.getSketchCodeFiles(filenames, null); - int fileCount = filenames.size(); + SketchCode[] codes = sketch.getCode(); - // Was considering keeping track of the last "known" number of files - // (instead of using sketch.getCodeCount() here) in case the user - // didn't want to reload after the number of files had changed. - // However, that's a bad situation anyway and there aren't good - // ways to recover or work around it, so just prompt the user again. - if (fileCount == sketch.getCodeCount()) { - return false; - } + // Separate codes with and without files + Map> existsMap = Arrays.stream(codes) + .collect(Collectors.groupingBy(code -> filenames.contains(code.getFileName()))); + + + // ADDED FILES + + List codeFilenames = Arrays.stream(codes) + .map(SketchCode::getFileName) + .collect(Collectors.toList()); + + // Get filenames which are in filesystem but don't have code + List 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 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 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(sketch.getName() + " file count now " + fileCount + - " instead of " + sketch.getCodeCount()); + 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); } - if (reloadPrompt()) { - if (sketch.getMainFile().exists()) { - reloadSketch(); - } else { - // If the main file was deleted, and that's why we're here, - // then we need to re-save the sketch instead. - try { - // Mark everything as modified so that it saves properly - for (SketchCode code : sketch.getCode()) { - code.setModified(true); + + // 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.save(); - } catch (Exception e) { - //if that didn't work, tell them it's un-recoverable - showErrorEDT("Reload Failed", - "The main file for this sketch was deleted\n" + - "and could not be rewritten.", e); - } - } - /* - if (fileCount < 1) { - // if they chose to reload and there aren't any files left - try { - // make a blank file for the main PDE - sketch.getMainFile().createNewFile(); - } catch (Exception e1) { - //if that didn't work, tell them it's un-recoverable - showErrorEDT("Reload failed", "The sketch contains no code files.", e1); - //don't try to reload again after the double fail - //this editor is probably trashed by this point, but a save-as might be possible -// skip = true; - return true; - } - // it's okay to do this without confirmation, because they already - // confirmed to deleting the unsaved changes above - sketch.reload(); - showWarningEDT("Modified Reload", - "You cannot delete the last code file in a sketch.\n" + - "A new blank sketch file has been generated for you."); - } - */ - } else { // !reload (user said no or closed the window) - // Because the number of files changed, they may be working with a file - // that doesn't exist any more. So find the files that are missing, - // and mark them as modified so that the next "Save" will write them. - for (SketchCode code : sketch.getCode()) { - if (!code.getFile().exists()) { - setCodeModified(code); - } - } - rebuildHeaderEDT(); - } - // Yes, we've brought this up with the user (so don't bother them further) - return true; - } + // Sketch was reloaded, clear ignore lists + ignoredAdditions.clear(); + ignoredRemovals.clear(); - - private void checkFileTimes() { - List reloadList = new ArrayList<>(); - for (SketchCode code : sketch.getCode()) { - File sketchFile = code.getFile(); - if (sketchFile.exists()) { - long diff = sketchFile.lastModified() - code.getLastModified(); - if (diff > MODIFICATION_WINDOW_MILLIS) { - if (DEBUG) System.out.println(sketchFile.getName() + " " + diff + "ms"); - reloadList.add(code); + return; } - } else { - // If a file in the sketch was not found, then it must have been - // deleted externally, so reload the sketch. - if (DEBUG) System.out.println(sketchFile.getName() + " (file disappeared)"); - reloadList.add(code); - } + + // 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); } - // If there are any files that need to be reloaded - if (reloadList.size() > 0) { - if (reloadPrompt()) { - reloadSketch(); - - } else { - // User said no, but take bulletproofing actions - for (SketchCode code : reloadList) { - // Set the file as modified in the Editor so the contents will - // save to disk when the user saves from inside Processing. - setCodeModified(code); - // Since this was canceled, update the "last modified" time so we - // don't ask the user about it again. - code.setLastModified(); - } - rebuildHeaderEDT(); - } - } - } - - - private void setCodeModified(SketchCode sc) { - sc.setModified(true); - sketch.setModified(true); - } - - - private void reloadSketch() { - sketch.reload(); - rebuildHeaderEDT(); } @@ -213,68 +208,12 @@ public class ChangeDetector implements WindowFocusListener { * perform the actual reload. * @return true if user said yes, false if they hit No or closed the window */ - private boolean reloadPrompt() { - int response = blockingYesNoPrompt(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."); + 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 showErrorEDT(final String title, final String message, - final Exception e) { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - Messages.showError(title, message, e); - } - }); - } - - - /* - private void showWarningEDT(final String title, final String message) { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - Messages.showWarning(title, message); - } - }); - } - */ - - - private int blockingYesNoPrompt(final Frame editor, final String title, - final String message1, - final String message2) { - final int[] result = { -1 }; // yuck - try { - //have to wait for a response on this one - EventQueue.invokeAndWait(new Runnable() { - @Override - public void run() { - result[0] = Messages.showYesNoQuestion(editor, title, message1, message2); - } - }); - } catch (InvocationTargetException e) { - //occurs if Base.showYesNoQuestion throws an error, so, shouldn't happen - e.getTargetException().printStackTrace(); - } catch (InterruptedException e) { - //occurs if the EDT is interrupted, so, shouldn't happen - e.printStackTrace(); - } - return result[0]; - } - - - private void rebuildHeaderEDT() { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - editor.header.rebuild(); - } - }); - } }