11package processing .app .ui ;
22
33import java .awt .EventQueue ;
4- import java .awt .Frame ;
54import java .awt .event .WindowEvent ;
65import java .awt .event .WindowFocusListener ;
7- import java .io .File ;
86import java .lang .reflect .InvocationTargetException ;
97import java .util .ArrayList ;
8+ import java .util .Arrays ;
9+ import java .util .Collections ;
1010import java .util .List ;
11+ import java .util .Map ;
12+ import java .util .Optional ;
13+ import java .util .concurrent .ForkJoinPool ;
14+ import java .util .stream .Collectors ;
15+ import java .util .stream .Stream ;
1116
1217import javax .swing .JOptionPane ;
1318
@@ -21,6 +26,9 @@ public class ChangeDetector implements WindowFocusListener {
2126 private final Sketch sketch ;
2227 private final Editor editor ;
2328
29+ private List <String > ignoredAdditions = new ArrayList <>();
30+ private List <SketchCode > ignoredRemovals = new ArrayList <>();
31+
2432 // Windows and others seem to have a few hundred ms difference in reported
2533 // times, so we're arbitrarily setting a gap in time here.
2634 // 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 {
3240 static private final boolean DEBUG =
3341 Preferences .getBoolean ("editor.watcher.debug" );
3442
35- // Store the known number of files to avoid re-asking about the same change
36- // private int lastKnownCount = -1;
37-
3843
3944 public ChangeDetector (Editor editor ) {
4045 this .sketch = editor .sketch ;
@@ -44,27 +49,19 @@ public ChangeDetector(Editor editor) {
4449
4550 @ Override
4651 public void windowGainedFocus (WindowEvent e ) {
47- // When the window is activated, fire off a Thread to check for changes
4852 if (Preferences .getBoolean ("editor.watcher" )) {
49- new Thread (new Runnable () {
50- @ Override
51- public void run () {
52- if (sketch != null ) {
53- // make sure the sketch folder exists at all.
54- // if it does not, it will be re-saved, and no changes will be detected
55- sketch .ensureExistence ();
56-
57- // if (lastKnownCount == -1) {
58- // lastKnownCount = sketch.getCodeCount();
59- // }
60-
61- boolean alreadyPrompted = checkFileCount ();
62- if (!alreadyPrompted ) {
63- checkFileTimes ();
64- }
65- }
66- }
67- }).start ();
53+ if (sketch != null ) {
54+ // make sure the sketch folder exists at all.
55+ // if it does not, it will be re-saved, and no changes will be detected
56+ sketch .ensureExistence (); // <- touches UI, stay on EDT
57+
58+ // TODO: Not sure if we even need to run this async. Usually takes
59+ // just a few ms and we probably want to prevent any changes from
60+ // users until the external changes are sorted out. [jv 2016-12-05]
61+
62+ // Run task in common pool, starting threads directly is so Java 6
63+ ForkJoinPool .commonPool ().execute (this ::checkFiles );
64+ }
6865 }
6966 }
7067
@@ -76,205 +73,147 @@ public void windowLostFocus(WindowEvent e) {
7673 }
7774
7875
79- private boolean checkFileCount () {
80- // check file count first
76+ // Synchronize, we are running async and touching fields
77+ private synchronized void checkFiles () {
8178
8279 List <String > filenames = new ArrayList <>();
83-
8480 sketch .getSketchCodeFiles (filenames , null );
8581
86- int fileCount = filenames . size ();
82+ SketchCode [] codes = sketch . getCode ();
8783
88- // Was considering keeping track of the last "known" number of files
89- // (instead of using sketch.getCodeCount() here) in case the user
90- // didn't want to reload after the number of files had changed.
91- // However, that's a bad situation anyway and there aren't good
92- // ways to recover or work around it, so just prompt the user again.
93- if (fileCount == sketch .getCodeCount ()) {
94- return false ;
95- }
84+ // Separate codes with and without files
85+ Map <Boolean , List <SketchCode >> existsMap = Arrays .stream (codes )
86+ .collect (Collectors .groupingBy (code -> filenames .contains (code .getFileName ())));
9687
97- if (DEBUG ) {
98- System .out .println (sketch .getName () + " file count now " + fileCount +
99- " instead of " + sketch .getCodeCount ());
100- }
10188
102- if (reloadPrompt ()) {
103- if (sketch .getMainFile ().exists ()) {
104- reloadSketch ();
105- } else {
106- // If the main file was deleted, and that's why we're here,
107- // then we need to re-save the sketch instead.
108- try {
109- // Mark everything as modified so that it saves properly
110- for (SketchCode code : sketch .getCode ()) {
111- code .setModified (true );
112- }
113- sketch .save ();
114- } catch (Exception e ) {
115- //if that didn't work, tell them it's un-recoverable
116- showErrorEDT ("Reload Failed" ,
117- "The main file for this sketch was deleted\n " +
118- "and could not be rewritten." , e );
119- }
120- }
89+ // ADDED FILES
12190
122- /*
123- if (fileCount < 1) {
124- // if they chose to reload and there aren't any files left
125- try {
126- // make a blank file for the main PDE
127- sketch.getMainFile().createNewFile();
128- } catch (Exception e1) {
129- //if that didn't work, tell them it's un-recoverable
130- showErrorEDT("Reload failed", "The sketch contains no code files.", e1);
131- //don't try to reload again after the double fail
132- //this editor is probably trashed by this point, but a save-as might be possible
133- // skip = true;
134- return true;
135- }
136- // it's okay to do this without confirmation, because they already
137- // confirmed to deleting the unsaved changes above
138- sketch.reload();
139- showWarningEDT("Modified Reload",
140- "You cannot delete the last code file in a sketch.\n" +
141- "A new blank sketch file has been generated for you.");
142- }
143- */
144- } else { // !reload (user said no or closed the window)
145- // Because the number of files changed, they may be working with a file
146- // that doesn't exist any more. So find the files that are missing,
147- // and mark them as modified so that the next "Save" will write them.
148- for (SketchCode code : sketch .getCode ()) {
149- if (!code .getFile ().exists ()) {
150- setCodeModified (code );
151- }
152- }
153- rebuildHeaderEDT ();
154- }
155- // Yes, we've brought this up with the user (so don't bother them further)
156- return true ;
157- }
91+ List <String > codeFilenames = Arrays .stream (codes )
92+ .map (SketchCode ::getFileName )
93+ .collect (Collectors .toList ());
15894
95+ // Get filenames which are in filesystem but don't have code
96+ List <String > addedFilenames = filenames .stream ()
97+ .filter (f -> !codeFilenames .contains (f ))
98+ .collect (Collectors .toList ());
15999
160- private void checkFileTimes () {
161- List <SketchCode > reloadList = new ArrayList <>();
162- for (SketchCode code : sketch .getCode ()) {
163- File sketchFile = code .getFile ();
164- if (sketchFile .exists ()) {
165- long diff = sketchFile .lastModified () - code .getLastModified ();
166- if (diff > MODIFICATION_WINDOW_MILLIS ) {
167- if (DEBUG ) System .out .println (sketchFile .getName () + " " + diff + "ms" );
168- reloadList .add (code );
169- }
170- } else {
171- // If a file in the sketch was not found, then it must have been
172- // deleted externally, so reload the sketch.
173- if (DEBUG ) System .out .println (sketchFile .getName () + " (file disappeared)" );
174- reloadList .add (code );
175- }
176- }
100+ // Show prompt if there are any added files which were not previously ignored
101+ boolean added = addedFilenames .stream ()
102+ .anyMatch (f -> !ignoredAdditions .contains (f ));
177103
178- // If there are any files that need to be reloaded
179- if (reloadList .size () > 0 ) {
180- if (reloadPrompt ()) {
181- reloadSketch ();
182-
183- } else {
184- // User said no, but take bulletproofing actions
185- for (SketchCode code : reloadList ) {
186- // Set the file as modified in the Editor so the contents will
187- // save to disk when the user saves from inside Processing.
188- setCodeModified (code );
189- // Since this was canceled, update the "last modified" time so we
190- // don't ask the user about it again.
191- code .setLastModified ();
192- }
193- rebuildHeaderEDT ();
194- }
195- }
196- }
197104
105+ // REMOVED FILES
198106
199- private void setCodeModified (SketchCode sc ) {
200- sc .setModified (true );
201- sketch .setModified (true );
202- }
107+ // Get codes which don't have file
108+ List <SketchCode > removedCodes = Optional .ofNullable (existsMap .get (Boolean .FALSE ))
109+ .orElse (Collections .emptyList ());
203110
111+ // Show prompt if there are any removed codes which were not previously ignored
112+ boolean removed = removedCodes .stream ()
113+ .anyMatch (code -> !ignoredRemovals .contains (code ));
204114
205- private void reloadSketch () {
206- sketch .reload ();
207- rebuildHeaderEDT ();
208- }
209115
116+ /// MODIFIED FILES
210117
211- /**
212- * Prompt the user whether to reload the sketch. If the user says yes,
213- * perform the actual reload.
214- * @return true if user said yes, false if they hit No or closed the window
215- */
216- private boolean reloadPrompt () {
217- int response = blockingYesNoPrompt (editor ,
218- "File Modified" ,
219- "Your sketch has been modified externally.<br>" +
220- "Would you like to reload the sketch?" ,
221- "If you reload the sketch, any unsaved changes will be lost." );
222- return response == JOptionPane .YES_OPTION ;
223- }
118+ // Get codes which have file with different modification time
119+ List <SketchCode > modifiedCodes = Optional .ofNullable (existsMap .get (Boolean .TRUE ))
120+ .orElse (Collections .emptyList ())
121+ .stream ()
122+ .filter (code -> {
123+ long fileLastModified = code .getFile ().lastModified ();
124+ long codeLastModified = code .getLastModified ();
125+ long diff = fileLastModified - codeLastModified ;
126+ return fileLastModified == 0L || diff > MODIFICATION_WINDOW_MILLIS ;
127+ })
128+ .collect (Collectors .toList ());
224129
130+ // Show prompt if any open codes were modified
131+ boolean modified = !modifiedCodes .isEmpty ();
225132
226- private void showErrorEDT (final String title , final String message ,
227- final Exception e ) {
228- EventQueue .invokeLater (new Runnable () {
229- @ Override
230- public void run () {
231- Messages .showError (title , message , e );
232- }
233- });
234- }
235133
134+ boolean ask = added || removed || modified ;
236135
237- /*
238- private void showWarningEDT(final String title, final String message) {
239- EventQueue.invokeLater(new Runnable() {
240- @Override
241- public void run() {
242- Messages.showWarning(title, message);
243- }
244- });
245- }
246- */
136+ if (DEBUG ) {
137+ System .out .println ("ask: " + ask + "\n " +
138+ "added filenames: " + addedFilenames + ",\n " +
139+ "ignored added: " + ignoredAdditions + ",\n " +
140+ "removed codes: " + removedCodes + ",\n " +
141+ "ignored removed: " + ignoredRemovals + ",\n " +
142+ "modified codes: " + modifiedCodes );
143+ }
247144
248145
249- private int blockingYesNoPrompt (final Frame editor , final String title ,
250- final String message1 ,
251- final String message2 ) {
252- final int [] result = { -1 }; // yuck
146+ // This has to happen in one go and also touches UI everywhere. It has to
147+ // run on EDT, otherwise windowGainedFocus callback runs again right after
148+ // dismissing the prompt and we get another prompt before we even finished.
253149 try {
254- //have to wait for a response on this one
255- EventQueue .invokeAndWait (new Runnable () {
256- @ Override
257- public void run () {
258- result [0 ] = Messages .showYesNoQuestion (editor , title , message1 , message2 );
150+ // Wait for EDT to finish its business
151+ // We need to stay in synchronized scope because of ignore lists
152+ EventQueue .invokeAndWait (() -> {
153+ // Show prompt if something interesting happened
154+ if (ask && showReloadPrompt ()) {
155+ // She said yes!!!
156+ if (sketch .getMainFile ().exists ()) {
157+ sketch .reload ();
158+ editor .rebuildHeader ();
159+ } else {
160+ // If the main file was deleted, and that's why we're here,
161+ // then we need to re-save the sketch instead.
162+ // Mark everything as modified so that it saves properly
163+ for (SketchCode code : codes ) {
164+ code .setModified (true );
165+ }
166+ try {
167+ sketch .save ();
168+ } catch (Exception e ) {
169+ //if that didn't work, tell them it's un-recoverable
170+ Messages .showError ("Reload Failed" , "The main file for this sketch was deleted\n " +
171+ "and could not be rewritten." , e );
172+ }
173+ }
174+
175+ // Sketch was reloaded, clear ignore lists
176+ ignoredAdditions .clear ();
177+ ignoredRemovals .clear ();
178+
179+ return ;
180+ }
181+
182+ // Update ignore lists to get rid of old stuff
183+ ignoredAdditions = addedFilenames ;
184+ ignoredRemovals = removedCodes ;
185+
186+ // If something changed, set modified flags and modification times
187+ if (!removedCodes .isEmpty () || !modifiedCodes .isEmpty ()) {
188+ Stream .concat (removedCodes .stream (), modifiedCodes .stream ())
189+ .forEach (code -> {
190+ code .setModified (true );
191+ code .setLastModified ();
192+ });
193+
194+ // Not sure if this is needed
195+ editor .rebuildHeader ();
259196 }
260197 });
198+ } catch (InterruptedException ignore ) {
261199 } catch (InvocationTargetException e ) {
262- //occurs if Base.showYesNoQuestion throws an error, so, shouldn't happen
263- e .getTargetException ().printStackTrace ();
264- } catch (InterruptedException e ) {
265- //occurs if the EDT is interrupted, so, shouldn't happen
266- e .printStackTrace ();
200+ Messages .loge ("exception in ChangeDetector" , e );
267201 }
268- return result [ 0 ];
202+
269203 }
270204
271205
272- private void rebuildHeaderEDT () {
273- EventQueue .invokeLater (new Runnable () {
274- @ Override
275- public void run () {
276- editor .header .rebuild ();
277- }
278- });
206+ /**
207+ * Prompt the user whether to reload the sketch. If the user says yes,
208+ * perform the actual reload.
209+ * @return true if user said yes, false if they hit No or closed the window
210+ */
211+ private boolean showReloadPrompt () {
212+ int response = Messages
213+ .showYesNoQuestion (editor , "File Modified" ,
214+ "Your sketch has been modified externally.<br>" +
215+ "Would you like to reload the sketch?" ,
216+ "If you reload the sketch, any unsaved changes will be lost." );
217+ return response == JOptionPane .YES_OPTION ;
279218 }
280219}
0 commit comments