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