Skip to content

Commit bd85ca3

Browse files
committed
Implement Excel window movement tracking
1 parent bb71a2a commit bd85ca3

File tree

9 files changed

+214
-20
lines changed

9 files changed

+214
-20
lines changed

Source/ExcelDna.IntelliSense/ExcelDna.IntelliSense.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
<SubType>Form</SubType>
9393
</Compile>
9494
<Compile Include="UIMonitor\UIMonitor.cs" />
95+
<Compile Include="UIMonitor\WindowLocationWatcher.cs" />
9596
<Compile Include="UIMonitor\WindowWatcher.cs" />
9697
<Compile Include="Win32Helper.cs" />
9798
<Compile Include="Providers\LoaderNotification.cs" />

Source/ExcelDna.IntelliSense/IntelliSenseDisplay.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ void FormulaEditTextChange(string formulaPrefix, Rect editWindowBounds, IntPtr e
294294
// We have a function name and we want to show info
295295
if (_argumentsToolTip != null)
296296
{
297-
// NOTE: Hiding just once doesn't help - the tooltip pops up again
297+
// NOTE: Hiding or moving just once doesn't help - the tooltip pops up in its original place again
298298
// TODO: Try to move it off-screen, behind or make invisible
299299
//if (!_argumentsToolTip.Visible)
300300
//{

Source/ExcelDna.IntelliSense/UIMonitor/FormulaEditWatcher.cs

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ void SetEditWindow(IntPtr newWindowHandle, ref IntPtr hwnd, ref AutomationElemen
209209
try
210210
{
211211
UninstallTextChangeMonitor(element);
212+
UninstallLocationMonitor();
212213
Logger.WindowWatcher.Verbose($"FormulaEdit Uninstalled event handlers for {hwnd}");
213214
}
214215
catch (Exception ex)
@@ -230,6 +231,8 @@ void SetEditWindow(IntPtr newWindowHandle, ref IntPtr hwnd, ref AutomationElemen
230231
try
231232
{
232233
InstallTextChangeMonitor(element);
234+
if (IsEditingFormula)
235+
InstallLocationMonitor(GetTopLevelWindow(element));
233236
Logger.WindowWatcher.Verbose($"FormulaEdit Installed event handlers for {newWindowHandle}");
234237
}
235238
catch (Exception ex)
@@ -317,6 +320,36 @@ void FormulaPollingCallback(object _unused_)
317320
}
318321
}
319322

323+
WindowLocationWatcher _windowLocationWatcher;
324+
325+
// Runs on our Automation thread
326+
void InstallLocationMonitor(IntPtr hWnd)
327+
{
328+
if (_windowLocationWatcher != null)
329+
{
330+
_windowLocationWatcher.Dispose();
331+
}
332+
_windowLocationWatcher = new WindowLocationWatcher(hWnd, _syncContextAuto);
333+
_windowLocationWatcher.LocationChanged += _windowLocationWatcher_LocationChanged;
334+
}
335+
336+
// Runs on our Automation thread
337+
void UninstallLocationMonitor()
338+
{
339+
if (_windowLocationWatcher != null)
340+
{
341+
_windowLocationWatcher.Dispose();
342+
_windowLocationWatcher = null;
343+
}
344+
}
345+
346+
// Runs on our Automation thread
347+
void _windowLocationWatcher_LocationChanged(object sender, EventArgs e)
348+
{
349+
UpdateEditState(moveOnly: true);
350+
_windowWatcher.OnFormulaEditLocationChanged();
351+
}
352+
320353
// Runs on an Automation event thread
321354
// CONSIDER: With WinEvents we could get notified from main thread ... ?
322355
void TextChanged(object sender, AutomationEventArgs e)
@@ -326,15 +359,7 @@ void TextChanged(object sender, AutomationEventArgs e)
326359
UpdateFormula(textChangedOnly: true);
327360
}
328361

329-
// Runs on an Automation event thread
330-
// CONSIDER: With WinEvents we could get notified from main thread ... ?
331-
void LocationChanged(object sender, AutomationPropertyChangedEventArgs e)
332-
{
333-
// Debug.Print($">>>> FormulaEditWatcher.LocationChanged on thread {Thread.CurrentThread.ManagedThreadId}");
334-
Logger.WindowWatcher.Verbose($"FormulaEdit - Location changed");
335-
UpdateEditState(true);
336-
}
337-
362+
// Switches to our Automation thread, updates current state and raises StateChanged event
338363
void UpdateEditState(bool moveOnly = false)
339364
{
340365
Logger.WindowWatcher.Verbose("> FormulaEdit UpdateEditState - Posted");
@@ -368,6 +393,8 @@ void UpdateEditState(bool moveOnly = false)
368393
// Neither have the focus, so we don't update anything
369394
Logger.WindowWatcher.Verbose("FormulaEdit UpdateEditState End formula editing");
370395
CurrentPrefix = null;
396+
if (IsEditingFormula)
397+
UninstallLocationMonitor();
371398
IsEditingFormula = false;
372399
prefixChanged = true;
373400
// Debug.Print("Don't have a focused text box to update.");
@@ -380,7 +407,10 @@ void UpdateEditState(bool moveOnly = false)
380407

381408
var pt = Win32Helper.GetClientCursorPos(hwnd);
382409
CaretPosition = new Point(pt.X, pt.Y);
410+
if (!IsEditingFormula)
411+
InstallLocationMonitor(GetTopLevelWindow(focusedEdit));
383412
IsEditingFormula = true;
413+
384414
var newPrefix = XlCall.GetFormulaEditPrefix(); // What thread do we have to use here ...?
385415
if (CurrentPrefix != newPrefix)
386416
{
@@ -391,7 +421,14 @@ void UpdateEditState(bool moveOnly = false)
391421
}
392422

393423
// TODO: Smarter notification...?
394-
OnStateChanged(new StateChangeEventArgs(((bool)moveOnlyObj && !prefixChanged) ? StateChangeType.Move : StateChangeType.Multiple));
424+
if ((bool)moveOnlyObj && !prefixChanged)
425+
{
426+
StateChanged?.Invoke(this, new StateChangeEventArgs(StateChangeType.Move));
427+
}
428+
else
429+
{
430+
OnStateChanged(new StateChangeEventArgs(StateChangeType.Multiple));
431+
}
395432
}, moveOnly);
396433
}
397434

@@ -429,5 +466,24 @@ public void Dispose()
429466
}, null);
430467
Logger.WindowWatcher.Verbose("FormulaEdit Dispose End");
431468
}
469+
470+
471+
static IntPtr GetTopLevelWindow(AutomationElement element)
472+
{
473+
TreeWalker walker = TreeWalker.ControlViewWalker;
474+
AutomationElement elementParent;
475+
AutomationElement node = element;
476+
if (node == AutomationElement.RootElement)
477+
return node.NativeElement.CurrentNativeWindowHandle;
478+
do
479+
{
480+
elementParent = walker.GetParent(node);
481+
if (elementParent == AutomationElement.RootElement)
482+
break;
483+
node = elementParent;
484+
}
485+
while (true);
486+
return node.NativeElement.CurrentNativeWindowHandle;
487+
}
432488
}
433489
}

Source/ExcelDna.IntelliSense/UIMonitor/PopupListWatcher.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public PopupListWatcher(WindowWatcher windowWatcher, SynchronizationContext sync
3131
_syncContextAuto = syncContextAuto;
3232
_windowWatcher = windowWatcher;
3333
_windowWatcher.PopupListWindowChanged += _windowWatcher_PopupListWindowChanged;
34+
_windowWatcher.FormulaEditLocationChanged += _windowWatcher_FormulaEditLocationChanged;
3435
}
3536

3637
// Runs on our automation thread
@@ -58,8 +59,8 @@ void _windowWatcher_PopupListWindowChanged(object sender, WindowWatcher.WindowCh
5859
try
5960
{
6061
// DO we need to remove...?
61-
if (_popupList != null)
62-
Automation.RemoveAutomationPropertyChangedEventHandler(_popupList, PopupListBoundsChanged);
62+
//if (_popupList != null)
63+
//Automation.RemoveAutomationPropertyChangedEventHandler(_popupList, PopupListBoundsChanged);
6364
}
6465
catch (Exception ex)
6566
{
@@ -97,6 +98,17 @@ void _windowWatcher_PopupListWindowChanged(object sender, WindowWatcher.WindowCh
9798
}
9899
}
99100

101+
// Runs on our automation thread
102+
void _windowWatcher_FormulaEditLocationChanged(object sender, EventArgs e)
103+
{
104+
if (IsVisible && _selectedItem != null)
105+
{
106+
SelectedItemBounds = (Rect)_selectedItem.GetCurrentPropertyValue(AutomationElement.BoundingRectangleProperty);
107+
ListBounds = (Rect)_popupList.GetCurrentPropertyValue(AutomationElement.BoundingRectangleProperty);
108+
OnSelectedItemChanged();
109+
}
110+
}
111+
100112
//// Runs on our automation thread
101113
//void _windowWatcher_MainWindowChanged(object sender, EventArgs args)
102114
//{
@@ -130,7 +142,7 @@ void PopupListBoundsChanged(object sender, AutomationPropertyChangedEventArgs e)
130142
if (e.NewValue != null)
131143
ListBounds = (Rect)e.NewValue;
132144

133-
// We don't have to trigger the update
145+
// We don't have to trigger the update, relying on the FormulaEdit to also have noticed the move...
134146

135147
//_syncContextAuto.Post(delegate
136148
//{
@@ -179,6 +191,7 @@ void InstallEventHandlers()
179191
Automation.AddAutomationEventHandler(
180192
SelectionItemPattern.ElementSelectedEvent, _popupList, TreeScope.Descendants /* was .Children */, PopupListElementSelectedHandler);
181193
Logger.WindowWatcher.Verbose($"PopupList selection event handler added");
194+
// NOTE: Using this event is pretty slow...
182195
//Automation.AddAutomationPropertyChangedEventHandler(_popupList, TreeScope.Element, PopupListBoundsChanged, AutomationElement.BoundingRectangleProperty);
183196
//Logger.WindowWatcher.Verbose($"PopupList bounds change event handler added");
184197
}

Source/ExcelDna.IntelliSense/UIMonitor/UIMonitor.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -558,11 +558,26 @@ void _formulaEditWatcher_StateChanged(object sender, FormulaEditWatcher.StateCha
558558
{
559559
var newState = ((UIState.FormulaEdit)CurrentState).WithFormulaPrefix(_formulaEditWatcher.CurrentPrefix);
560560
OnStateChanged(newState);
561+
return;
561562
}
562-
else
563+
564+
if (args.StateChangeType == FormulaEditWatcher.StateChangeType.Move)
563565
{
564-
OnStateChanged();
566+
if (CurrentState is UIState.FunctionList)
567+
{
568+
// We'll update stuff in the PopupList change handler
569+
// var newState = ((UIState.FunctionList)CurrentState).WithBounds(_formulaEditWatcher.EditWindowBounds);
570+
// OnStateChanged(newState);
571+
return;
572+
}
573+
if (CurrentState is UIState.FormulaEdit)
574+
{
575+
var newState = ((UIState.FormulaEdit)CurrentState).WithBounds(_formulaEditWatcher.EditWindowBounds);
576+
OnStateChanged(newState);
577+
return;
578+
}
565579
}
580+
OnStateChanged();
566581
}
567582

568583
void _excelToolTipWatcher_ToolTipChanged(object sender, ExcelToolTipWatcher.ToolTipChangeEventArgs e)

Source/ExcelDna.IntelliSense/UIMonitor/WinEvents.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ public enum WinEvent : uint
7575
EVENT_OBJECT_TEXTSELECTIONCHANGED = 0x8014, // hwnd ID idChild is item w? test selection change
7676
EVENT_OBJECT_CONTENTSCROLLED = 0x8015,
7777
EVENT_SYSTEM_ARRANGMENTPREVIEW = 0x8016,
78+
EVENT_SYSTEM_MOVESIZESTART = 0x000A,
79+
EVENT_SYSTEM_MOVESIZEEND = 0x000B, // The movement or resizing of a window has finished. This event is sent by the system, never by servers.
80+
// There are slso events about minimize / restore
7881
EVENT_OBJECT_END = 0x80FF,
7982
EVENT_AIA_START = 0xA000,
8083
EVENT_AIA_END = 0xAFFF,
@@ -84,13 +87,15 @@ public enum WinEvent : uint
8487

8588
readonly IntPtr _hWinEventHook;
8689
readonly SynchronizationContext _syncContextAuto;
90+
readonly IntPtr _hWndFilterOrZero; // If non-zero, only these window events are processed
8791
readonly WinEventDelegate _handleWinEventDelegate; // Ensures delegate that we pass to SetWinEventHook is not GC'd
8892

89-
public WinEventHook(WinEvent eventMin, WinEvent eventMax, SynchronizationContext syncContextAuto)
93+
public WinEventHook(WinEvent eventMin, WinEvent eventMax, SynchronizationContext syncContextAuto, IntPtr hWndFilterOrZero)
9094
{
9195
if (syncContextAuto == null)
9296
throw new ArgumentNullException(nameof(syncContextAuto));
9397
_syncContextAuto = syncContextAuto;
98+
_hWndFilterOrZero = hWndFilterOrZero;
9499
var xllModuleHandle = Win32Helper.GetXllModuleHandle();
95100
var excelProcessId = Win32Helper.GetExcelProcessId();
96101
_handleWinEventDelegate = HandleWinEvent;
@@ -113,6 +118,9 @@ void HandleWinEvent(IntPtr hWinEventHook, WinEvent eventType, IntPtr hWnd,
113118
if (disposedValue)
114119
return;
115120

121+
if (_hWndFilterOrZero != IntPtr.Zero && hWnd != _hWndFilterOrZero)
122+
return;
123+
116124
// CONSIDER: We might add some filtering here... maybe only interested in some of the window / event combinations
117125
_syncContextAuto.Post(OnWinEventReceived, new WinEventArgs(eventType, hWnd, idObject, idChild, dwEventThread, dwmsEventTime));
118126
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Threading;
5+
6+
namespace ExcelDna.IntelliSense
7+
{
8+
public class WindowLocationWatcher : IDisposable
9+
{
10+
IntPtr _hWnd;
11+
SynchronizationContext _syncContextAuto;
12+
WinEventHook _windowMoveSizeHook;
13+
WinEventHook _windowLocationChangeHook;
14+
15+
public event EventHandler LocationChanged;
16+
17+
public WindowLocationWatcher(IntPtr hWnd, SynchronizationContext syncContextAuto)
18+
{
19+
_hWnd = hWnd;
20+
_syncContextAuto = syncContextAuto;
21+
_windowMoveSizeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_SYSTEM_MOVESIZESTART, WinEventHook.WinEvent.EVENT_SYSTEM_MOVESIZEEND, _syncContextAuto, _hWnd);
22+
_windowMoveSizeHook.WinEventReceived += _windowMoveHook_WinEventReceived;
23+
}
24+
25+
void _windowMoveHook_WinEventReceived(object sender, WinEventHook.WinEventArgs winEventArgs)
26+
{
27+
#if DEBUG
28+
Logger.WinEvents.Verbose($"{winEventArgs.EventType} - Window {winEventArgs.WindowHandle:X} ({Win32Helper.GetClassName(winEventArgs.WindowHandle)} - Object/Child {winEventArgs.ObjectId} / {winEventArgs.ChildId} - Thread {winEventArgs.EventThreadId} at {winEventArgs.EventTimeMs}");
29+
#endif
30+
if (winEventArgs.EventType == WinEventHook.WinEvent.EVENT_SYSTEM_MOVESIZESTART)
31+
{
32+
if (_windowLocationChangeHook != null)
33+
{
34+
Debug.Fail("Unexpected move start without end");
35+
_windowLocationChangeHook.Dispose();
36+
}
37+
_windowLocationChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE, WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE, _syncContextAuto, _hWnd);
38+
_windowLocationChangeHook.WinEventReceived += _windowLocationChangeHook_WinEventReceived;
39+
}
40+
else if (winEventArgs.EventType == WinEventHook.WinEvent.EVENT_SYSTEM_MOVESIZEEND)
41+
{
42+
_windowLocationChangeHook.Dispose();
43+
_windowLocationChangeHook = null;
44+
}
45+
}
46+
47+
void _windowLocationChangeHook_WinEventReceived(object sender, WinEventHook.WinEventArgs winEventArgs)
48+
{
49+
#if DEBUG
50+
Logger.WinEvents.Verbose($"{winEventArgs.EventType} - Window {winEventArgs.WindowHandle:X} ({Win32Helper.GetClassName(winEventArgs.WindowHandle)} - Object/Child {winEventArgs.ObjectId} / {winEventArgs.ChildId} - Thread {winEventArgs.EventThreadId} at {winEventArgs.EventTimeMs}");
51+
#endif
52+
LocationChanged?.Invoke(this, EventArgs.Empty);
53+
}
54+
55+
public void Dispose()
56+
{
57+
if (_windowMoveSizeHook != null)
58+
{
59+
_windowMoveSizeHook.Dispose();
60+
_windowMoveSizeHook = null;
61+
}
62+
if (_windowLocationChangeHook != null)
63+
{
64+
_windowLocationChangeHook.Dispose();
65+
_windowLocationChangeHook = null;
66+
}
67+
}
68+
}
69+
}

Source/ExcelDna.IntelliSense/UIMonitor/WindowWatcher.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public enum ChangeType
2626
Show = 3,
2727
Hide = 4,
2828
Focus = 5,
29-
Unfocus = 6
29+
Unfocus = 6,
30+
LocationChange = 7
3031
}
3132

3233
public readonly IntPtr WindowHandle;
@@ -58,6 +59,9 @@ internal WindowChangedEventArgs(IntPtr windowHandle, WinEventHook.WinEvent winEv
5859
case WinEventHook.WinEvent.EVENT_OBJECT_FOCUS:
5960
Type = ChangeType.Focus;
6061
break;
62+
case WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE:
63+
Type = ChangeType.LocationChange;
64+
break;
6165
default:
6266
throw new ArgumentException("Unexpected WinEvent type", nameof(winEvent));
6367
}
@@ -69,7 +73,8 @@ internal static bool IsSupportedWinEvent(WinEventHook.WinEvent winEvent)
6973
winEvent == WinEventHook.WinEvent.EVENT_OBJECT_DESTROY ||
7074
winEvent == WinEventHook.WinEvent.EVENT_OBJECT_SHOW ||
7175
winEvent == WinEventHook.WinEvent.EVENT_OBJECT_HIDE ||
72-
winEvent == WinEventHook.WinEvent.EVENT_OBJECT_FOCUS;
76+
winEvent == WinEventHook.WinEvent.EVENT_OBJECT_FOCUS ||
77+
winEvent == WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE; // Only for the on-demand hook
7378
}
7479
}
7580

@@ -98,6 +103,7 @@ internal static bool IsSupportedWinEvent(WinEventHook.WinEvent winEvent)
98103
public event EventHandler<WindowChangedEventArgs> InCellEditWindowChanged;
99104
public event EventHandler<WindowChangedEventArgs> PopupListWindowChanged;
100105
public event EventHandler<WindowChangedEventArgs> ExcelToolTipWindowChanged;
106+
public event EventHandler FormulaEditLocationChanged;
101107
// public event EventHandler<WindowChangedEventArgs> SelectDataSourceWindowChanged;
102108

103109
public WindowWatcher(SynchronizationContext syncContextAuto)
@@ -108,7 +114,7 @@ public WindowWatcher(SynchronizationContext syncContextAuto)
108114

109115
// Using WinEvents instead of Automation so that we can watch top-level window changes, but only from the right (current Excel) process.
110116
// TODO: We need to dramatically reduce the number of events we grab here...
111-
_windowStateChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_CREATE, WinEventHook.WinEvent.EVENT_OBJECT_FOCUS, syncContextAuto);
117+
_windowStateChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_CREATE, WinEventHook.WinEvent.EVENT_OBJECT_FOCUS, syncContextAuto, IntPtr.Zero);
112118
// _windowStateChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_CREATE, WinEventHook.WinEvent.EVENT_OBJECT_END, syncContextAuto);
113119

114120
_windowStateChangeHook.WinEventReceived += _windowStateChangeHook_WinEventReceived;
@@ -241,6 +247,13 @@ void _windowStateChangeHook_WinEventReceived(object sender, WinEventHook.WinEven
241247
}
242248
}
243249

250+
// Fired from the FormulaEditWatcher...
251+
// CONSIDER: We might restructure the location watching, so that it happens here, rather than in the FormulaEdit
252+
internal void OnFormulaEditLocationChanged()
253+
{
254+
FormulaEditLocationChanged?.Invoke(this, EventArgs.Empty);
255+
}
256+
244257
public void Dispose()
245258
{
246259
if (_windowStateChangeHook != null)

0 commit comments

Comments
 (0)