Skip to content

Commit 59d86e9

Browse files
authored
Merge pull request processing#4762 from JakubValtar/fix-infinite-popups
Clean up ChangeDetector
2 parents 261ae12 + 0cb42f9 commit 59d86e9

File tree

1 file changed

+133
-194
lines changed

1 file changed

+133
-194
lines changed
Lines changed: 133 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package processing.app.ui;
22

33
import java.awt.EventQueue;
4-
import java.awt.Frame;
54
import java.awt.event.WindowEvent;
65
import java.awt.event.WindowFocusListener;
7-
import java.io.File;
86
import java.lang.reflect.InvocationTargetException;
97
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.Collections;
1010
import 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

1217
import 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

Comments
 (0)