|
| 1 | +Custom Context Menu SpellCheck |
| 2 | +=== |
| 3 | + |
| 4 | +# Background |
| 5 | + |
| 6 | +When a host application renders a custom context menu via the `ContextMenuRequested` event, spellcheck |
| 7 | +suggestions for misspelled words are not available. The browser's built-in spellcheck pipeline resolves |
| 8 | +suggestions asynchronously, but there is no mechanism for custom context menu hosts to retrieve or apply |
| 9 | +these suggestions. |
| 10 | + |
| 11 | +# Description |
| 12 | + |
| 13 | +We propose extending the existing `ContextMenuRequested` API surface with spellcheck support for custom |
| 14 | +context menus. This adds the ability to: |
| 15 | + |
| 16 | +1. Query spellcheck information (misspelled word, readiness state, suggestions) from the context menu target. |
| 17 | +2. Subscribe to an asynchronous notification when spellcheck suggestions become available. |
| 18 | +3. Apply a selected spellcheck suggestion to replace the misspelled word in the DOM. |
| 19 | + |
| 20 | +Hosts opt in by calling `QueryInterface` for the new `ICoreWebView2ContextMenuTarget2` and |
| 21 | +`ICoreWebView2ContextMenuRequestedEventArgs2` interfaces. Existing `ContextMenuRequested` consumers |
| 22 | +are unaffected. |
| 23 | + |
| 24 | +# Examples |
| 25 | + |
| 26 | +## Win32 C++ |
| 27 | + |
| 28 | +```cpp |
| 29 | +void ShowCustomContextMenuWithSpellCheck( |
| 30 | + ICoreWebView2ContextMenuRequestedEventArgs* args) |
| 31 | +{ |
| 32 | + wil::com_ptr<ICoreWebView2ContextMenuTarget> target; |
| 33 | + CHECK_FAILURE(args->get_ContextMenuTarget(&target)); |
| 34 | + |
| 35 | + BOOL isEditable = FALSE; |
| 36 | + CHECK_FAILURE(target->get_IsEditable(&isEditable)); |
| 37 | + |
| 38 | + HMENU hMenu = CreatePopupMenu(); |
| 39 | + UINT menuIndex = 0; |
| 40 | + |
| 41 | + if (isEditable) |
| 42 | + { |
| 43 | + auto target2 = target.try_query<ICoreWebView2ContextMenuTarget2>(); |
| 44 | + auto args2 = wil::com_ptr_query<ICoreWebView2ContextMenuRequestedEventArgs2>(args); |
| 45 | + |
| 46 | + if (target2 && args2) |
| 47 | + { |
| 48 | + wil::unique_cotaskmem_string misspelledWord; |
| 49 | + COREWEBVIEW2_SPELL_CHECK_READINESS spellState; |
| 50 | + wil::com_ptr<ICoreWebView2StringCollection> suggestions; |
| 51 | + CHECK_FAILURE(target2->GetSpellCheckInfo( |
| 52 | + &misspelledWord, &spellState, &suggestions)); |
| 53 | + |
| 54 | + if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) |
| 55 | + { |
| 56 | + // Suggestions are available — add them to the menu. |
| 57 | + UINT32 count = 0; |
| 58 | + suggestions->get_Count(&count); |
| 59 | + for (UINT32 i = 0; i < count && i < 5; i++) |
| 60 | + { |
| 61 | + wil::unique_cotaskmem_string suggestion; |
| 62 | + suggestions->GetValueAtIndex(i, &suggestion); |
| 63 | + MENUITEMINFO mii = {}; |
| 64 | + mii.cbSize = sizeof(mii); |
| 65 | + mii.fMask = MIIM_STRING | MIIM_ID; |
| 66 | + mii.wID = IDM_SPELL_SUGGESTION_BASE + i; |
| 67 | + mii.dwTypeData = suggestion.get(); |
| 68 | + InsertMenuItem(hMenu, menuIndex++, TRUE, &mii); |
| 69 | + } |
| 70 | + } |
| 71 | + else if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY) |
| 72 | + { |
| 73 | + // Suggestions pending — show placeholder and register async handler. |
| 74 | + MENUITEMINFO mii = {}; |
| 75 | + mii.cbSize = sizeof(mii); |
| 76 | + mii.fMask = MIIM_STRING | MIIM_ID | MIIM_STATE; |
| 77 | + mii.fState = MFS_DISABLED; |
| 78 | + mii.wID = IDM_SPELL_PLACEHOLDER; |
| 79 | + mii.dwTypeData = const_cast<LPWSTR>(L"Loading suggestions..."); |
| 80 | + InsertMenuItem(hMenu, menuIndex++, TRUE, &mii); |
| 81 | + |
| 82 | + // Handler fires when suggestions arrive (or immediately if |
| 83 | + // already resolved). During TrackPopupMenu's modal loop, |
| 84 | + // the handler updates the menu in-place. |
| 85 | + CHECK_FAILURE(args2->add_SpellCheckSuggestionsReady( |
| 86 | + Callback<ICoreWebView2SpellCheckSuggestionsReadyEventHandler>( |
| 87 | + [hMenu, target2, args2]( |
| 88 | + ICoreWebView2ContextMenuRequestedEventArgs* sender, |
| 89 | + IUnknown* eventArgs) -> HRESULT |
| 90 | + { |
| 91 | + wil::unique_cotaskmem_string word; |
| 92 | + COREWEBVIEW2_SPELL_CHECK_READINESS state; |
| 93 | + wil::com_ptr<ICoreWebView2StringCollection> suggs; |
| 94 | + CHECK_FAILURE(target2->GetSpellCheckInfo( |
| 95 | + &word, &state, &suggs)); |
| 96 | + |
| 97 | + if (state == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) |
| 98 | + { |
| 99 | + UINT32 count = 0; |
| 100 | + suggs->get_Count(&count); |
| 101 | + if (count > 0) |
| 102 | + { |
| 103 | + // Replace placeholder with first suggestion. |
| 104 | + wil::unique_cotaskmem_string first; |
| 105 | + suggs->GetValueAtIndex(0, &first); |
| 106 | + MENUITEMINFO mii = {}; |
| 107 | + mii.cbSize = sizeof(mii); |
| 108 | + mii.fMask = MIIM_STRING | MIIM_ID | MIIM_STATE; |
| 109 | + mii.fState = MFS_ENABLED; |
| 110 | + mii.wID = IDM_SPELL_SUGGESTION_BASE; |
| 111 | + mii.dwTypeData = first.get(); |
| 112 | + SetMenuItemInfo( |
| 113 | + hMenu, IDM_SPELL_PLACEHOLDER, FALSE, &mii); |
| 114 | + |
| 115 | + // Insert remaining suggestions. |
| 116 | + for (UINT32 i = 1; i < count && i < 5; i++) |
| 117 | + { |
| 118 | + wil::unique_cotaskmem_string s; |
| 119 | + suggs->GetValueAtIndex(i, &s); |
| 120 | + MENUITEMINFO item = {}; |
| 121 | + item.cbSize = sizeof(item); |
| 122 | + item.fMask = MIIM_STRING | MIIM_ID; |
| 123 | + item.wID = IDM_SPELL_SUGGESTION_BASE + i; |
| 124 | + item.dwTypeData = s.get(); |
| 125 | + InsertMenuItem(hMenu, i, TRUE, &item); |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + // Repaint the popup menu. |
| 130 | + HWND hPopup = FindWindow(L"#32768", nullptr); |
| 131 | + if (hPopup) |
| 132 | + { |
| 133 | + RedrawWindow( |
| 134 | + hPopup, nullptr, nullptr, |
| 135 | + RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE); |
| 136 | + } |
| 137 | + } |
| 138 | + return S_OK; |
| 139 | + }) |
| 140 | + .Get(), |
| 141 | + &m_spellCheckToken)); |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + // Add standard WebView2 context menu items. |
| 147 | + wil::com_ptr<ICoreWebView2ContextMenuItemCollection> items; |
| 148 | + CHECK_FAILURE(args->get_MenuItems(&items)); |
| 149 | + UINT32 itemCount; |
| 150 | + CHECK_FAILURE(items->get_Count(&itemCount)); |
| 151 | + for (UINT32 i = 0; i < itemCount; i++) |
| 152 | + { |
| 153 | + wil::com_ptr<ICoreWebView2ContextMenuItem> item; |
| 154 | + CHECK_FAILURE(items->GetValueAtIndex(i, &item)); |
| 155 | + // ... add each item to hMenu ... |
| 156 | + } |
| 157 | + |
| 158 | + // Show the menu. |
| 159 | + UINT selectedId = TrackPopupMenu( |
| 160 | + hMenu, TPM_RETURNCMD, pt.x, pt.y, 0, m_hWnd, nullptr); |
| 161 | + |
| 162 | + // Handle selection. |
| 163 | + if (selectedId >= IDM_SPELL_SUGGESTION_BASE && |
| 164 | + selectedId < IDM_SPELL_SUGGESTION_BASE + 5) |
| 165 | + { |
| 166 | + // Apply the selected spellcheck suggestion. |
| 167 | + wil::unique_cotaskmem_string word; |
| 168 | + COREWEBVIEW2_SPELL_CHECK_READINESS state; |
| 169 | + wil::com_ptr<ICoreWebView2StringCollection> suggs; |
| 170 | + target2->GetSpellCheckInfo(&word, &state, &suggs); |
| 171 | + wil::unique_cotaskmem_string chosen; |
| 172 | + suggs->GetValueAtIndex(selectedId - IDM_SPELL_SUGGESTION_BASE, &chosen); |
| 173 | + args2->ApplySpellCheckSuggestion(chosen.get()); |
| 174 | + } |
| 175 | + |
| 176 | + args->put_Handled(TRUE); |
| 177 | + DestroyMenu(hMenu); |
| 178 | +} |
| 179 | +``` |
| 180 | +
|
| 181 | +## C#/.NET |
| 182 | +
|
| 183 | +```csharp |
| 184 | +void ShowCustomContextMenuWithSpellCheck( |
| 185 | + object sender, CoreWebView2ContextMenuRequestedEventArgs args) |
| 186 | +{ |
| 187 | + var target = args.ContextMenuTarget; |
| 188 | + var menuItems = new List<ToolStripItem>(); |
| 189 | +
|
| 190 | + if (target.IsEditable) |
| 191 | + { |
| 192 | + string misspelledWord = target.MisspelledWord; |
| 193 | + CoreWebView2SpellCheckReadiness spellState = target.SpellCheckReadiness; |
| 194 | + IReadOnlyList<string> suggestions = target.SpellCheckSuggestions; |
| 195 | +
|
| 196 | + if (spellState == CoreWebView2SpellCheckReadiness.Ready && suggestions.Count > 0) |
| 197 | + { |
| 198 | + // Suggestions available — add them directly. |
| 199 | + foreach (string suggestion in suggestions.Take(5)) |
| 200 | + { |
| 201 | + var item = new ToolStripMenuItem(suggestion); |
| 202 | + item.Click += (s, e) => |
| 203 | + { |
| 204 | + args.ApplySpellCheckSuggestion(suggestion); |
| 205 | + }; |
| 206 | + menuItems.Add(item); |
| 207 | + } |
| 208 | + menuItems.Add(new ToolStripSeparator()); |
| 209 | + } |
| 210 | + else if (spellState == CoreWebView2SpellCheckReadiness.NotReady) |
| 211 | + { |
| 212 | + // Suggestions pending — show placeholder. |
| 213 | + var placeholder = new ToolStripMenuItem("Loading suggestions...") |
| 214 | + { |
| 215 | + Enabled = false |
| 216 | + }; |
| 217 | + menuItems.Add(placeholder); |
| 218 | + menuItems.Add(new ToolStripSeparator()); |
| 219 | +
|
| 220 | + // Register async handler. Fires when suggestions resolve |
| 221 | + // or immediately if already resolved. |
| 222 | + args.SpellCheckSuggestionsReady += (s, e) => |
| 223 | + { |
| 224 | + string word = target.MisspelledWord; |
| 225 | + var readySuggestions = target.SpellCheckSuggestions; |
| 226 | + if (target.SpellCheckReadiness == CoreWebView2SpellCheckReadiness.Ready |
| 227 | + && readySuggestions.Count > 0) |
| 228 | + { |
| 229 | + // Replace placeholder with actual suggestions. |
| 230 | + int index = menuItems.IndexOf(placeholder); |
| 231 | + menuItems.Remove(placeholder); |
| 232 | + foreach (string suggestion in readySuggestions.Take(5)) |
| 233 | + { |
| 234 | + var item = new ToolStripMenuItem(suggestion); |
| 235 | + item.Click += (s2, e2) => |
| 236 | + { |
| 237 | + args.ApplySpellCheckSuggestion(suggestion); |
| 238 | + }; |
| 239 | + menuItems.Insert(index++, item); |
| 240 | + } |
| 241 | + } |
| 242 | + }; |
| 243 | + } |
| 244 | + } |
| 245 | +
|
| 246 | + // Add standard WebView2 context menu items. |
| 247 | + foreach (var menuItem in args.MenuItems) |
| 248 | + { |
| 249 | + // ... add each item to menuItems ... |
| 250 | + } |
| 251 | +
|
| 252 | + // Show the context menu. |
| 253 | + var contextMenu = new ContextMenuStrip(); |
| 254 | + contextMenu.Items.AddRange(menuItems.ToArray()); |
| 255 | + contextMenu.Show(webView, webView.PointToClient(Cursor.Position)); |
| 256 | +
|
| 257 | + args.Handled = true; |
| 258 | +} |
| 259 | +``` |
| 260 | + |
| 261 | +# API Details |
| 262 | + |
| 263 | +## Win32 C++ |
| 264 | + |
| 265 | +```idl |
| 266 | +/// Indicates the readiness of spellcheck suggestions for the context menu |
| 267 | +/// target. Used by hosts rendering custom context menus. |
| 268 | +[v1_enum] |
| 269 | +typedef enum COREWEBVIEW2_SPELL_CHECK_READINESS { |
| 270 | + /// Spellcheck suggestions are available and ready to display. |
| 271 | + COREWEBVIEW2_SPELL_CHECK_READINESS_READY, |
| 272 | + /// Spellcheck is active but suggestions have not yet been resolved |
| 273 | + /// (asynchronous retrieval in progress). |
| 274 | + COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY, |
| 275 | + /// Spellcheck suggestions are not applicable for the current context |
| 276 | + /// (not editable, no misspelling, or spellcheck disabled). |
| 277 | + COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE, |
| 278 | + /// Spellcheck resolution failed due to an internal error. |
| 279 | + /// No suggestions will arrive. |
| 280 | + COREWEBVIEW2_SPELL_CHECK_READINESS_ERROR, |
| 281 | +} COREWEBVIEW2_SPELL_CHECK_READINESS; |
| 282 | +
|
| 283 | +/// Receives `SpellCheckSuggestionsReady` events from |
| 284 | +/// ICoreWebView2ContextMenuRequestedEventArgs2. |
| 285 | +[uuid(c5d6e7f8-9a0b-1c2d-3e4f-5a6b7c8d9e0f), object, pointer_default(unique)] |
| 286 | +interface ICoreWebView2SpellCheckSuggestionsReadyEventHandler : IUnknown { |
| 287 | + /// Provides the event args for the corresponding event. |
| 288 | + HRESULT Invoke( |
| 289 | + [in] ICoreWebView2ContextMenuRequestedEventArgs* sender, |
| 290 | + [in] IUnknown* args); |
| 291 | +} |
| 292 | +
|
| 293 | +/// Extends ICoreWebView2ContextMenuTarget with spellcheck information |
| 294 | +/// for custom context menu integration. Allows host applications to retrieve |
| 295 | +/// spellcheck suggestions and metadata when rendering custom menus |
| 296 | +/// on editable fields. |
| 297 | +[uuid(d3f7e01a-9b5c-4e8f-a1d2-7c6b3e4f5a80), object, pointer_default(unique)] |
| 298 | +interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { |
| 299 | + /// Gets spellcheck information for the current context menu target in a |
| 300 | + /// single call. Returns the misspelled word (empty if none), the readiness |
| 301 | + /// state of suggestions, and a collection of suggestion strings. |
| 302 | + /// |
| 303 | + /// The caller must free `misspelledWord` with `CoTaskMemFree`. |
| 304 | + /// |
| 305 | + /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_READY`, the |
| 306 | + /// `suggestions` collection is populated with correction strings. |
| 307 | + /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY`, the |
| 308 | + /// collection is empty; subscribe to `SpellCheckSuggestionsReady` on |
| 309 | + /// `ICoreWebView2ContextMenuRequestedEventArgs2` to be notified when |
| 310 | + /// suggestions become available, then call this method again. |
| 311 | + /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE` or |
| 312 | + /// `COREWEBVIEW2_SPELL_CHECK_READINESS_ERROR`, no suggestions will arrive. |
| 313 | + HRESULT GetSpellCheckInfo( |
| 314 | + [out] LPWSTR* misspelledWord, |
| 315 | + [out] COREWEBVIEW2_SPELL_CHECK_READINESS* state, |
| 316 | + [out, retval] ICoreWebView2StringCollection** suggestions); |
| 317 | +} |
| 318 | +
|
| 319 | +/// Extends ICoreWebView2ContextMenuRequestedEventArgs with methods to apply |
| 320 | +/// spellcheck corrections and subscribe to asynchronous suggestion delivery. |
| 321 | +[uuid(e4a8f3b2-6c1d-4e9a-b5f7-2d8c9a0e1b34), object, pointer_default(unique)] |
| 322 | +interface ICoreWebView2ContextMenuRequestedEventArgs2 |
| 323 | + : ICoreWebView2ContextMenuRequestedEventArgs { |
| 324 | + /// Applies the selected spellcheck suggestion by replacing the misspelled |
| 325 | + /// word in the currently focused editable field. The `suggestion` parameter |
| 326 | + /// must be one of the strings obtained from the `suggestions` collection |
| 327 | + /// returned by `ICoreWebView2ContextMenuTarget2::GetSpellCheckInfo`. |
| 328 | + /// The runtime handles all editing internally, including routing to the |
| 329 | + /// correct frame for nested iframes. |
| 330 | + HRESULT ApplySpellCheckSuggestion([in] LPCWSTR suggestion); |
| 331 | +
|
| 332 | + /// Registers an event handler for `SpellCheckSuggestionsReady`. This fires |
| 333 | + /// when asynchronous spellcheck suggestions become available. If suggestions |
| 334 | + /// are already in `READY` state at registration time, the handler fires |
| 335 | + /// immediately and synchronously. |
| 336 | + HRESULT add_SpellCheckSuggestionsReady( |
| 337 | + [in] ICoreWebView2SpellCheckSuggestionsReadyEventHandler* eventHandler, |
| 338 | + [out] EventRegistrationToken* token); |
| 339 | +
|
| 340 | + /// Removes the event handler previously added with |
| 341 | + /// `add_SpellCheckSuggestionsReady`. |
| 342 | + HRESULT remove_SpellCheckSuggestionsReady( |
| 343 | + [in] EventRegistrationToken token); |
| 344 | +} |
| 345 | +``` |
| 346 | + |
| 347 | +## .NET/C# |
| 348 | + |
| 349 | +```csharp |
| 350 | +namespace Microsoft.Web.WebView2.Core |
| 351 | +{ |
| 352 | + enum CoreWebView2SpellCheckReadiness |
| 353 | + { |
| 354 | + Ready = 0, |
| 355 | + NotReady = 1, |
| 356 | + NotAvailable = 2, |
| 357 | + Error = 3, |
| 358 | + }; |
| 359 | + |
| 360 | + runtimeclass CoreWebView2ContextMenuTarget |
| 361 | + { |
| 362 | + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuTarget2")] |
| 363 | + { |
| 364 | + String MisspelledWord { get; }; |
| 365 | + CoreWebView2SpellCheckReadiness SpellCheckReadiness { get; }; |
| 366 | + IVectorView<String> SpellCheckSuggestions { get; }; |
| 367 | + } |
| 368 | + } |
| 369 | + |
| 370 | + runtimeclass CoreWebView2ContextMenuRequestedEventArgs |
| 371 | + { |
| 372 | + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuRequestedEventArgs2")] |
| 373 | + { |
| 374 | + void ApplySpellCheckSuggestion(String suggestion); |
| 375 | + event Windows.Foundation.TypedEventHandler< |
| 376 | + CoreWebView2ContextMenuRequestedEventArgs, Object> |
| 377 | + SpellCheckSuggestionsReady; |
| 378 | + } |
| 379 | + } |
| 380 | +} |
| 381 | +``` |
0 commit comments