@@ -83,6 +83,10 @@ public class ClientInput
8383 private List <Runnable > disposeJobs = new ArrayList <>();
8484 private List <ClientInputListener > listeners = new ArrayList <>();
8585
86+ private final Object pendingLock = new Object ();
87+ private boolean pendingDirty ;
88+ private boolean pendingRecalculate ;
89+
8690 @ Inject
8791 private IEventBroker broker ;
8892
@@ -148,6 +152,37 @@ public void touch()
148152 setDirty (true , false );
149153 }
150154
155+ private void scheduleDirty (boolean recalculate )
156+ {
157+ if (Display .getDefault ().getThread () == Thread .currentThread ())
158+ {
159+ setDirty (true , recalculate );
160+ return ;
161+ }
162+ synchronized (pendingLock )
163+ {
164+ pendingRecalculate |= recalculate ;
165+ if (pendingDirty )
166+ return ; // asyncExec already queued; it will pick up the merged state
167+ pendingDirty = true ;
168+ }
169+ Display .getDefault ().asyncExec (this ::applyPendingDirty );
170+ }
171+
172+ private void applyPendingDirty ()
173+ {
174+ boolean recalculate ;
175+ synchronized (pendingLock )
176+ {
177+ if (!pendingDirty )
178+ return ; // consumed by a save() that ran before we fired
179+ recalculate = pendingRecalculate ;
180+ pendingDirty = false ;
181+ pendingRecalculate = false ;
182+ }
183+ setDirty (true , recalculate );
184+ }
185+
151186 private void setDirty (boolean isDirty , boolean recalculate )
152187 {
153188 this .isDirty = isDirty ;
@@ -230,7 +265,21 @@ public void save(Shell shell)
230265 storePreferences (false );
231266
232267 broker .post (UIConstants .Event .File .SAVED , clientFile .getAbsolutePath ());
233- setDirty (false , false );
268+
269+ // Cancel any pending asyncExec dirty notification. If a background
270+ // modification arrived during the save the on-disk file may not reflect
271+ // it, so we conservatively remain dirty in that case.
272+ boolean hadPendingModification ;
273+ boolean hadPendingRecalculate ;
274+ synchronized (pendingLock )
275+ {
276+ hadPendingModification = pendingDirty ;
277+ hadPendingRecalculate = pendingRecalculate ;
278+ pendingDirty = false ;
279+ pendingRecalculate = false ;
280+ }
281+ setDirty (hadPendingModification , hadPendingRecalculate );
282+
234283 listeners .forEach (ClientInputListener ::onSaved );
235284 }
236285 catch (IOException e )
@@ -269,7 +318,18 @@ public void doSaveAs(Shell shell, String extension, Set<SaveFlag> flags) // NOSO
269318 storePreferences (true );
270319
271320 broker .post (UIConstants .Event .File .SAVED , clientFile .getAbsolutePath ());
272- setDirty (false , false );
321+
322+ boolean hadPendingModification ;
323+ boolean hadPendingRecalculate ;
324+ synchronized (pendingLock )
325+ {
326+ hadPendingModification = pendingDirty ;
327+ hadPendingRecalculate = pendingRecalculate ;
328+ pendingDirty = false ;
329+ pendingRecalculate = false ;
330+ }
331+ setDirty (hadPendingModification , hadPendingRecalculate );
332+
273333 listeners .forEach (ClientInputListener ::onSaved );
274334 }
275335 catch (IOException e )
@@ -658,15 +718,7 @@ private void scheduleAutoSaveJob()
658718 // convenience: Client#markDirty can be called on any thread, but
659719 // ClientInputListener#onDirty will always be called on the UI
660720 // thread
661-
662- if (Display .getDefault ().getThread () == Thread .currentThread ())
663- {
664- setDirty (true , recalculate );
665- }
666- else
667- {
668- Display .getDefault ().asyncExec (() -> setDirty (true , recalculate ));
669- }
721+ scheduleDirty (recalculate );
670722 };
671723 client .addPropertyChangeListener (listener );
672724 disposeJobs .add (() -> client .removePropertyChangeListener (listener ));
0 commit comments