From 19fc936a7f64f5e6f54b3d2b40c3f95bd9749d17 Mon Sep 17 00:00:00 2001 From: George Bateman Date: Sat, 22 Apr 2017 15:29:18 +0100 Subject: [PATCH] Make the change detector not reload the sketch Fixes #4713, fixes #4849 --- app/src/processing/app/Messages.java | 50 ++-- app/src/processing/app/Sketch.java | 38 ++- app/src/processing/app/ui/ChangeDetector.java | 217 +++++++++++++----- build/shared/lib/languages/PDE.properties | 12 + 4 files changed, 244 insertions(+), 73 deletions(-) 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