diff --git a/Source/ExcelDna.IntelliSense/ExcelDna.IntelliSense.csproj b/Source/ExcelDna.IntelliSense/ExcelDna.IntelliSense.csproj index 4e2e07a..0edbcfc 100644 --- a/Source/ExcelDna.IntelliSense/ExcelDna.IntelliSense.csproj +++ b/Source/ExcelDna.IntelliSense/ExcelDna.IntelliSense.csproj @@ -88,6 +88,7 @@ + diff --git a/Source/ExcelDna.IntelliSense/UIMonitor/FormulaEditWatcher.cs b/Source/ExcelDna.IntelliSense/UIMonitor/FormulaEditWatcher.cs index c7a9eff..dd71b67 100644 --- a/Source/ExcelDna.IntelliSense/UIMonitor/FormulaEditWatcher.cs +++ b/Source/ExcelDna.IntelliSense/UIMonitor/FormulaEditWatcher.cs @@ -1,4 +1,5 @@ -using System; +using ExcelDna.IntelliSense.Util; +using System; using System.Diagnostics; using System.Threading; using System.Windows; @@ -71,11 +72,15 @@ public IntPtr FormulaEditWindow readonly SynchronizationContext _syncContextMain; readonly WindowWatcher _windowWatcher; // Passed in - WindowLocationWatcher _windowLocationWatcher; // Managed here - IntPtr _hwndFormulaBar; - IntPtr _hwndInCellEdit; - FormulaEditFocus _formulaEditFocus; + WindowLocationWatcher _windowLocationWatcher; // Managed here + readonly RenewableDelayExecutor _updateEditStateAfterTimeout; + + IntPtr _hwndFormulaBar; + IntPtr _hwndInCellEdit; + FormulaEditFocus _formulaEditFocus; + + const int DelayBeforeStateUpdateMilliseconds = 100; public FormulaEditWatcher(WindowWatcher windowWatcher, SynchronizationContext syncContextAuto, SynchronizationContext syncContextMain) { @@ -84,6 +89,7 @@ public FormulaEditWatcher(WindowWatcher windowWatcher, SynchronizationContext sy _windowWatcher = windowWatcher; _windowWatcher.FormulaBarWindowChanged += _windowWatcher_FormulaBarWindowChanged; _windowWatcher.InCellEditWindowChanged += _windowWatcher_InCellEditWindowChanged; + _updateEditStateAfterTimeout = new RenewableDelayExecutor(DelayBeforeStateUpdateMilliseconds, () => UpdateEditState()); } // Runs on the Automation thread @@ -95,13 +101,13 @@ void _windowWatcher_FormulaBarWindowChanged(object sender, WindowWatcher.WindowC if (e.ObjectId == WindowWatcher.WindowChangedEventArgs.ChangeObjectId.Self) { SetEditWindow(e.WindowHandle, ref _hwndFormulaBar); - UpdateEditState(); + _updateEditStateAfterTimeout.Signal(); } else if (e.ObjectId == WindowWatcher.WindowChangedEventArgs.ChangeObjectId.Caret) { // We expect this on every text change // NOTE: Not anymore after some Excel / Windows update - UpdateEditStateDelayed(); + _updateEditStateAfterTimeout.Signal(); } else { @@ -126,7 +132,7 @@ void _windowWatcher_FormulaBarWindowChanged(object sender, WindowWatcher.WindowC SetEditWindow(e.WindowHandle, ref _hwndFormulaBar); } _formulaEditFocus = FormulaEditFocus.FormulaBar; - UpdateEditState(); + _updateEditStateAfterTimeout.Signal(); } break; case WindowWatcher.WindowChangedEventArgs.ChangeType.Unfocus: @@ -134,21 +140,21 @@ void _windowWatcher_FormulaBarWindowChanged(object sender, WindowWatcher.WindowC { Logger.WindowWatcher.Verbose($"FormulaEdit - FormulaBar Unfocus"); _formulaEditFocus = FormulaEditFocus.None; - UpdateEditState(); + _updateEditStateAfterTimeout.Signal(); } break; case WindowWatcher.WindowChangedEventArgs.ChangeType.Show: - Logger.WindowWatcher.Verbose($"FormulaEdit - FormulaBar Show"); + Logger.WindowWatcher.Verbose($"FormulaEdit - FormulaBar Show"); break; case WindowWatcher.WindowChangedEventArgs.ChangeType.Hide: - Logger.WindowWatcher.Verbose($"FormulaEdit - FormulaBar Hide"); + Logger.WindowWatcher.Verbose($"FormulaEdit - FormulaBar Hide"); break; case WindowWatcher.WindowChangedEventArgs.ChangeType.LocationChange: if (e.ObjectId == WindowWatcher.WindowChangedEventArgs.ChangeObjectId.Caret) { // We expect this on every text change in newer Excel versions Debug.Print($"-#-#-#- Text Changed ... "); - UpdateEditStateDelayed(); + _updateEditStateAfterTimeout.Signal(); } else { @@ -172,14 +178,14 @@ void _windowWatcher_InCellEditWindowChanged(object sender, WindowWatcher.WindowC if (e.ObjectId == WindowWatcher.WindowChangedEventArgs.ChangeObjectId.Self) { SetEditWindow(e.WindowHandle, ref _hwndInCellEdit); - UpdateEditState(); + _updateEditStateAfterTimeout.Signal(); } else if (e.ObjectId == WindowWatcher.WindowChangedEventArgs.ChangeObjectId.Caret) { // We expect this on every text change // NOTE: Not anymore after some Excel / Windows update Debug.Print($"-#-#-#- Text Changed ... "); - UpdateEditStateDelayed(); + _updateEditStateAfterTimeout.Signal(); } else { @@ -206,7 +212,7 @@ void _windowWatcher_InCellEditWindowChanged(object sender, WindowWatcher.WindowC Logger.WindowWatcher.Verbose($"FormulaEdit - InCellEdit Focus"); _formulaEditFocus = FormulaEditFocus.InCellEdit; - UpdateEditState(); + _updateEditStateAfterTimeout.Signal(); } break; case WindowWatcher.WindowChangedEventArgs.ChangeType.Unfocus: @@ -214,7 +220,7 @@ void _windowWatcher_InCellEditWindowChanged(object sender, WindowWatcher.WindowC { Logger.WindowWatcher.Verbose($"FormulaEdit - InCellEdit Unfocus"); _formulaEditFocus = FormulaEditFocus.None; - UpdateEditState(); + _updateEditStateAfterTimeout.Signal(); } break; case WindowWatcher.WindowChangedEventArgs.ChangeType.Show: @@ -228,7 +234,7 @@ void _windowWatcher_InCellEditWindowChanged(object sender, WindowWatcher.WindowC { // We expect this on every text change in newer Excel versions Debug.Print($"-#-#-#- Text Changed ... "); - UpdateEditStateDelayed(); + _updateEditStateAfterTimeout.Signal(); } else { @@ -313,22 +319,20 @@ void _windowLocationWatcher_LocationChanged(object sender, EventArgs e) // UpdateFormula(textChangedOnly: true); //} - // TODO: Get rid of this somehow - added to make the mouse clicks in the in-cell editing work, by delaying the call to the PenHelper - void UpdateEditStateDelayed() + void UpdateEditState(bool moveOnly = false) { - _syncContextAuto.Post(_ => - { - Thread.Sleep(100); - UpdateEditState(); - }, null); + // Switches to our Main UI thread, updates current state and raises StateChanged event + _syncContextMain.Post(_ => + { + UpdateEditStateImpl(moveOnly); + }, null); } - // Switches to our Automation thread, updates current state and raises StateChanged event - void UpdateEditState(bool moveOnly = false) + void UpdateEditStateImpl(bool moveOnly = false) { Logger.WindowWatcher.Verbose($"> FormulaEdit UpdateEditState - Thread {Thread.CurrentThread.ManagedThreadId}"); Logger.WindowWatcher.Verbose($"FormulaEdit UpdateEditState - Focus: {_formulaEditFocus} Window: {(_formulaEditFocus == FormulaEditFocus.FormulaBar ? _hwndFormulaBar : _hwndInCellEdit)}"); - + IntPtr hwnd = IntPtr.Zero; bool prefixChanged = false; if (_formulaEditFocus == FormulaEditFocus.FormulaBar) @@ -410,6 +414,9 @@ public void Dispose() Debug.Assert(Thread.CurrentThread.ManagedThreadId == 1); Logger.WindowWatcher.Verbose("FormulaEdit Dispose Begin"); + + _updateEditStateAfterTimeout.Dispose(); + _windowWatcher.FormulaBarWindowChanged -= _windowWatcher_FormulaBarWindowChanged; _windowWatcher.InCellEditWindowChanged -= _windowWatcher_InCellEditWindowChanged; @@ -419,7 +426,8 @@ public void Dispose() { tempWatcher.Dispose(); } + Logger.WindowWatcher.Verbose("FormulaEdit Dispose End"); } } -} +} \ No newline at end of file diff --git a/Source/ExcelDna.IntelliSense/Util/RenewableDelayExecutor.cs b/Source/ExcelDna.IntelliSense/Util/RenewableDelayExecutor.cs new file mode 100644 index 0000000..405cdee --- /dev/null +++ b/Source/ExcelDna.IntelliSense/Util/RenewableDelayExecutor.cs @@ -0,0 +1,67 @@ +using System; +using System.Timers; + +namespace ExcelDna.IntelliSense.Util +{ + /// + /// Upon a signal, executes the specified action after the specified delay. + /// If any other signal arrives during the waiting period, the delay interval begins anew. + /// + internal class RenewableDelayExecutor : IDisposable + { + private readonly Timer _timer; + private readonly Action _action; + private readonly int _debounceIntervalMilliseconds; + + public bool IsDisposed { get; private set; } + + public RenewableDelayExecutor(int debounceIntervalMilliseconds, Action action) + { + _action = action; + _debounceIntervalMilliseconds = debounceIntervalMilliseconds; + _timer = new Timer + { + AutoReset = false, + Interval = _debounceIntervalMilliseconds, + }; + + _timer.Elapsed += OnTimerElapsed; + } + + private void OnTimerElapsed(object sender, ElapsedEventArgs e) + { + if (IsDisposed) + { + return; + } + + _action(); + } + + public void Signal() + { + _timer.Stop(); + _timer.Start(); + } + + private void Dispose(bool isDisposing) + { + IsDisposed = true; + + _timer.Elapsed -= OnTimerElapsed; + _timer.Dispose(); + + if (isDisposing) + { + GC.SuppressFinalize(this); + } + } + + public void Dispose() => Dispose(true); + + ~RenewableDelayExecutor() + { + Dispose(false); + } + } +} \ No newline at end of file