Skip to content

Commit 3ab0c67

Browse files
Merge pull request #5553 from MicrosoftEdge/user/kumaranurag/custom_context_menu_spellcheck_integration
API Review: Custom context menu Spellcheck
2 parents f2c9e60 + 5e37ac0 commit 3ab0c67

1 file changed

Lines changed: 327 additions & 0 deletions

File tree

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
Spell Check Support for Custom Context Menus
2+
===
3+
4+
# Background
5+
6+
When a host application renders a custom context menu via the `ContextMenuRequested` event, spell check
7+
suggestions for misspelled words are not available. The browser's built-in spell check pipeline resolves
8+
suggestions asynchronously, but there is no mechanism for custom context menu hosts to retrieve or apply
9+
these suggestions.
10+
11+
This feature adds spell check support to custom context menus by extending
12+
`ICoreWebView2ContextMenuTarget` with a new `ICoreWebView2ContextMenuTarget2` interface. The host
13+
checks whether a misspelled word is present, then asynchronously retrieves spelling suggestions.
14+
15+
# Description
16+
17+
The `ICoreWebView2ContextMenuTarget` is extended with `ICoreWebView2ContextMenuTarget2`.
18+
This new interface provides:
19+
20+
- **`HasSpellingError`** — Read-only BOOL property indicating whether the context menu target
21+
contains a spelling error. This is always available synchronously when the event fires.
22+
- **`GetSpellCheckSuggestions(handler)`** — Asynchronously retrieves spell check suggestions as
23+
`ICoreWebView2ContextMenuItem` objects. Each suggestion has a `Label` (display text) and
24+
`CommandId` (opaque identifier).
25+
26+
**Runtime version detection:** If `QueryInterface` (QI) for `Target2` returns `E_NOINTERFACE`, the host
27+
is running on an older runtime that does not support this feature.
28+
29+
**Why async?** Spell check suggestions are resolved asynchronously by the platform spell checker
30+
(e.g., Windows `ISpellChecker`). When `ContextMenuRequested` fires, suggestions may not yet be
31+
available. `GetSpellCheckSuggestions` handles this transparently — it invokes the handler
32+
immediately if suggestions are ready, or waits for the platform spell checker to deliver them.
33+
If the platform spell checker does not respond within an internal timeout, the handler is invoked
34+
with an empty collection.
35+
36+
**Commanding model:** The host applies a suggestion by passing its `CommandId` to
37+
`put_SelectedCommandId` on the EventArgs — the same execution path used for Cut, Copy, Paste, and
38+
all other context menu items. No separate execution method is needed.
39+
40+
# Examples
41+
42+
## Win32 C++
43+
44+
```cpp
45+
webView->add_ContextMenuRequested(
46+
Callback<ICoreWebView2ContextMenuRequestedEventHandler>(
47+
[this](ICoreWebView2* sender,
48+
ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT
49+
{
50+
wil::com_ptr<ICoreWebView2ContextMenuTarget> target;
51+
CHECK_FAILURE(args->get_ContextMenuTarget(&target));
52+
53+
// QI for Target2 — returns E_NOINTERFACE on older runtimes.
54+
auto target2 = wil::try_com_query<
55+
ICoreWebView2ContextMenuTarget2>(target);
56+
if (!target2)
57+
return S_OK;
58+
59+
// Check if the context menu target has a spelling error.
60+
BOOL hasSpellingError = FALSE;
61+
CHECK_FAILURE(target2->get_HasSpellingError(&hasSpellingError));
62+
if (!hasSpellingError)
63+
return S_OK;
64+
65+
// Take deferral — menu will be shown after async callback.
66+
wil::com_ptr<ICoreWebView2Deferral> deferral;
67+
CHECK_FAILURE(args->GetDeferral(&deferral));
68+
CHECK_FAILURE(args->put_Handled(true));
69+
70+
// Asynchronously retrieve spell check suggestions.
71+
CHECK_FAILURE(target2->GetSpellCheckSuggestions(
72+
Callback<
73+
ICoreWebView2GetSpellCheckSuggestionsCompletedHandler>(
74+
[args, deferral](
75+
HRESULT errorCode,
76+
ICoreWebView2ContextMenuItemCollection*
77+
suggestions) -> HRESULT
78+
{
79+
// Enumerate suggestions — each has Label and CommandId.
80+
UINT32 count = 0;
81+
if (SUCCEEDED(errorCode) && suggestions)
82+
suggestions->get_Count(&count);
83+
84+
for (UINT32 i = 0; i < count; i++)
85+
{
86+
wil::com_ptr<ICoreWebView2ContextMenuItem> item;
87+
suggestions->GetValueAtIndex(i, &item);
88+
wil::unique_cotaskmem_string label;
89+
item->get_Label(&label);
90+
INT32 cmdId;
91+
item->get_CommandId(&cmdId);
92+
// ... add to custom menu using label and cmdId ...
93+
}
94+
95+
// Apply selection via unified commanding.
96+
// args->put_SelectedCommandId(selectedCmdId);
97+
98+
deferral->Complete();
99+
return S_OK;
100+
})
101+
.Get()));
102+
return S_OK;
103+
})
104+
.Get(),
105+
&m_contextMenuRequestedToken);
106+
```
107+
108+
## .NET/WinRT
109+
110+
```csharp
111+
webView.CoreWebView2.ContextMenuRequested += async (sender, args) =>
112+
{
113+
var target = args.ContextMenuTarget;
114+
115+
// Check if the context menu target has a spelling error.
116+
if (!target.HasSpellingError)
117+
return;
118+
119+
// Take deferral — menu will be shown after async call completes.
120+
var deferral = args.GetDeferral();
121+
args.Handled = true;
122+
123+
// Asynchronously retrieve spell check suggestions.
124+
IReadOnlyList<CoreWebView2ContextMenuItem> suggestions =
125+
await target.GetSpellCheckSuggestionsAsync();
126+
127+
// Build custom menu with suggestions.
128+
var contextMenu = new ContextMenuStrip();
129+
foreach (var suggestion in suggestions)
130+
{
131+
var item = new ToolStripMenuItem(suggestion.Label);
132+
var capturedId = suggestion.CommandId;
133+
item.Click += (_, _) =>
134+
{
135+
// Apply selection via unified commanding.
136+
args.SelectedCommandId = capturedId;
137+
};
138+
contextMenu.Items.Add(item);
139+
}
140+
141+
// Show menu and complete deferral when closed.
142+
contextMenu.Closed += (_, _) => deferral.Complete();
143+
contextMenu.Show(webView, new Point(args.Location.X, args.Location.Y));
144+
};
145+
```
146+
147+
# API Details
148+
149+
## Win32 COM IDL
150+
151+
```idl
152+
// ─── ContextMenuTarget2: Spell check support ───
153+
154+
/// Extends `ICoreWebView2ContextMenuTarget` with spell check support for
155+
/// custom context menus.
156+
///
157+
/// The host can `QueryInterface` the `ICoreWebView2ContextMenuTarget` returned
158+
/// by `ICoreWebView2ContextMenuRequestedEventArgs::get_ContextMenuTarget` to
159+
/// obtain this interface. Check `HasSpellingError` to determine whether
160+
/// the context menu was invoked on a misspelled word, then call
161+
/// `GetSpellCheckSuggestions` to asynchronously retrieve spelling corrections.
162+
///
163+
/// To apply a suggestion, pass the selected item's `CommandId` to
164+
/// `ICoreWebView2ContextMenuRequestedEventArgs::put_SelectedCommandId`.
165+
[uuid(f7a3b8c1-2d4e-5f6a-8b9c-0d1e2f3a4b5c), object, pointer_default(unique)]
166+
interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget {
167+
/// Returns TRUE if the context menu target contains a spelling error.
168+
/// When TRUE, call `GetSpellCheckSuggestions` to retrieve the available
169+
/// spelling correction suggestions asynchronously.
170+
[propget] HRESULT HasSpellingError([out, retval] BOOL* value);
171+
172+
/// Asynchronously retrieves spell check suggestion options as a collection
173+
/// of context menu items. The handler is invoked immediately if suggestions
174+
/// are already available, or when they become available from the platform
175+
/// spell check engine. Each item's `Label` is the suggestion text and its
176+
/// `CommandId` can be passed to `put_SelectedCommandId` to apply the
177+
/// correction. The handler receives an empty collection if no suggestions
178+
/// are available, if `HasSpellingError` is FALSE, or if the underlying
179+
/// spell check service does not respond within an internal timeout.
180+
/// Multiple concurrent calls are supported; each handler will be invoked
181+
/// with the same result when suggestions become available.
182+
/// Returns `E_POINTER` if `handler` is null.
183+
HRESULT GetSpellCheckSuggestions(
184+
[in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler);
185+
}
186+
187+
/// Receives the result of the `GetSpellCheckSuggestions` method.
188+
[uuid(d73832f9-d05b-438d-bb6d-6441245221e3), object, pointer_default(unique)]
189+
interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown {
190+
/// Provides the result of the corresponding asynchronous method.
191+
/// Each item in the `suggestions` collection is an
192+
/// `ICoreWebView2ContextMenuItem` whose `Label` is the suggestion text
193+
/// and whose `CommandId` uniquely identifies it. To apply a suggestion,
194+
/// pass the selected item's `CommandId` to
195+
/// `ICoreWebView2ContextMenuRequestedEventArgs.put_SelectedCommandId`.
196+
HRESULT Invoke(
197+
[in] HRESULT errorCode,
198+
[in] ICoreWebView2ContextMenuItemCollection* suggestions);
199+
}
200+
```
201+
202+
## .NET/WinRT
203+
204+
```csharp
205+
namespace Microsoft.Web.WebView2.Core
206+
{
207+
runtimeclass CoreWebView2ContextMenuTarget
208+
{
209+
// Existing members unchanged.
210+
211+
[interface_name("ICoreWebView2ContextMenuTarget2")]
212+
{
213+
/// <summary>
214+
/// Returns TRUE if the context menu target contains a spelling error.
215+
/// </summary>
216+
Boolean HasSpellingError { get; };
217+
218+
/// <summary>
219+
/// Asynchronously retrieves spell check suggestions. Each item's
220+
/// CommandId can be passed to SelectedCommandId to apply the correction.
221+
/// </summary>
222+
Windows.Foundation.IAsyncOperation<IVectorView<CoreWebView2ContextMenuItem>>
223+
GetSpellCheckSuggestionsAsync();
224+
}
225+
}
226+
}
227+
```
228+
229+
# Behavioral Details
230+
231+
## Discovery Flow
232+
233+
| Step | Action | Result |
234+
|------|--------|--------|
235+
| 1 | QI for `Target2` from `ContextMenuTarget` | `E_NOINTERFACE` → old runtime, fall back to default menu |
236+
| 2 | Read `HasSpellingError` | `TRUE` → spelling error present; `FALSE` → no spelling error |
237+
| 3 | Call `GetSpellCheckSuggestions(handler)` | Handler invoked when suggestions are available |
238+
239+
## Suggestion Item Properties
240+
241+
Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestions` has:
242+
243+
| Property | Value |
244+
|----------|-------|
245+
| `Label` | Suggestion text (e.g., "the") |
246+
| `CommandId` | WebView2-allocated opaque ID (e.g., 50001) |
247+
| `Name` | `"spellCheckSuggestion"` |
248+
| `Kind` | `COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND` |
249+
| `IsEnabled` | true |
250+
| `IsChecked` | false |
251+
| `Icon` | null |
252+
| `ShortcutKeyDescription` | empty string |
253+
| `Children` | null |
254+
255+
## Async Timing
256+
257+
Spell check suggestions are resolved asynchronously by the platform spell checker in the browser
258+
process. When `ContextMenuRequested` fires, the suggestions may be:
259+
260+
| State | Meaning | `GetSpellCheckSuggestions` behavior |
261+
|-------|---------|-------------------------------------|
262+
| **Ready** | Suggestions already resolved before the event fired | Handler invoked immediately |
263+
| **Not Ready** | Platform spell checker still working | Handler stored; invoked when browser delivers results via IPC, or after internal timeout with empty collection |
264+
265+
The host does **not** need to check readiness — `GetSpellCheckSuggestions` handles both cases
266+
transparently. In the typical case, the platform spell checker responds within a few milliseconds.
267+
The internal timeout is a conservative safeguard for rare scenarios where the platform spell checker
268+
is slow or unresponsive.
269+
270+
### Host Patterns
271+
272+
**Pattern 1: Wait-then-show** (simpler — used in the examples above)
273+
274+
The host defers the context menu, calls `GetSpellCheckSuggestions`, and builds/shows the menu
275+
only after the handler fires. This produces a complete menu in one shot but delays appearance
276+
if suggestions are not yet ready.
277+
278+
```
279+
ContextMenuRequested → put_Handled(TRUE) + GetDeferral → GetSpellCheckSuggestions
280+
→ [handler fires] → build & show menu → complete deferral
281+
```
282+
283+
**Pattern 2: Show-then-update** (responsive — mirrors browser built-in behavior)
284+
285+
The host shows the context menu immediately with a placeholder (e.g., "Loading suggestions…")
286+
and updates it in-place when the handler fires. This keeps menu appearance instant at the cost
287+
of added complexity. Since the host owns the custom context menu UI, it can modify the menu
288+
while it is open.
289+
290+
```
291+
ContextMenuRequested → put_Handled(TRUE) + GetDeferral → show menu with placeholder
292+
→ GetSpellCheckSuggestions → [handler fires] → update menu items in-place
293+
→ [user selects] → complete deferral
294+
```
295+
296+
Either pattern is valid. Pattern 1 is recommended for most hosts because the delay is typically
297+
imperceptible (suggestions often resolve before the event fires or within a few milliseconds
298+
after). Pattern 2 is appropriate for hosts that require guaranteed instant menu appearance.
299+
300+
# Appendix
301+
302+
## Planned Spell Check Extensions
303+
304+
The following actions will be added as additional `ICoreWebView2ContextMenuItem` entries in the
305+
collection returned by `GetSpellCheckSuggestions`. No new interfaces or methods are required:
306+
307+
| Action | `Name` value |
308+
|--------|-------------|
309+
| Add to Dictionary | `"spellCheckAddToDictionary"` |
310+
| Ignore (session) | `"spellCheckIgnore"` |
311+
312+
These follow the same commanding model: the host renders them like any other item and applies via
313+
`SelectedCommandId`. A `Language` property (BCP-47 tag of the dictionary that flagged the misspelling)
314+
may also be added to `ICoreWebView2ContextMenuTarget2` in a follow-up version. Profile-level
315+
spell check configuration (`IsSpellCheckEnabled`, `SpellCheckLanguages`) is tracked as a separate
316+
follow-up.
317+
318+
## Relationship to Existing APIs
319+
320+
| Existing API | This Feature |
321+
|-------------|-------------|
322+
| `EventArgs.MenuItems` | Synchronous snapshot of menu items |
323+
| `EventArgs.SelectedCommandId` | Execution path — now also used for spell check suggestions |
324+
| `ContextMenuItem.CommandId` | Already used for all items — spell check items join this pool |
325+
| `ContextMenuItem.Label` | Display text — spell check suggestions use this for the suggestion word |
326+
| `EventArgs.GetDeferral()` | Must be held across the async `GetSpellCheckSuggestions` gap |
327+
| `ContextMenuTarget` | Base target — QI to `Target2` for spell check support |

0 commit comments

Comments
 (0)