diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md new file mode 100644 index 00000000..64f087e3 --- /dev/null +++ b/specs/CustomContextMenuSpellcheck.md @@ -0,0 +1,327 @@ +Spell Check Support for Custom Context Menus +=== + +# Background + +When a host application renders a custom context menu via the `ContextMenuRequested` event, spell check +suggestions for misspelled words are not available. The browser's built-in spell check pipeline resolves +suggestions asynchronously, but there is no mechanism for custom context menu hosts to retrieve or apply +these suggestions. + +This feature adds spell check support to custom context menus by extending +`ICoreWebView2ContextMenuTarget` with a new `ICoreWebView2ContextMenuTarget2` interface. The host +checks whether a misspelled word is present, then asynchronously retrieves spelling suggestions. + +# Description + +The `ICoreWebView2ContextMenuTarget` is extended with `ICoreWebView2ContextMenuTarget2`. +This new interface provides: + +- **`HasMisspelledWord`** — Read-only BOOL property indicating whether the context menu target + contains a misspelled word. This is always available synchronously when the event fires. +- **`GetSpellCheckSuggestions(handler)`** — Asynchronously retrieves spell check suggestions as + `ICoreWebView2ContextMenuItem` objects. Each suggestion has a `Label` (display text) and + `CommandId` (opaque identifier). + +**Runtime version detection:** If `QueryInterface` (QI) for `Target2` returns `E_NOINTERFACE`, the host +is running on an older runtime that does not support this feature. + +**Why async?** Spell check suggestions are resolved asynchronously by the platform spell checker +(e.g., Windows `ISpellChecker`). When `ContextMenuRequested` fires, suggestions may not yet be +available. `GetSpellCheckSuggestions` handles this transparently — it invokes the handler +immediately if suggestions are ready, or waits for the platform spell checker to deliver them. +If the platform spell checker does not respond within an internal timeout, the handler is invoked +with an empty collection. + +**Commanding model:** The host applies a suggestion by passing its `CommandId` to +`put_SelectedCommandId` on the EventArgs — the same execution path used for Cut, Copy, Paste, and +all other context menu items. No separate execution method is needed. + +# Examples + +## Win32 C++ + +```cpp +webView->add_ContextMenuRequested( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT + { + wil::com_ptr target; + CHECK_FAILURE(args->get_ContextMenuTarget(&target)); + + // QI for Target2 — returns E_NOINTERFACE on older runtimes. + auto target2 = wil::try_com_query< + ICoreWebView2ContextMenuTarget2>(target); + if (!target2) + return S_OK; + + // Check if the context menu target has a misspelled word. + BOOL hasMisspelledWord = FALSE; + CHECK_FAILURE(target2->get_HasMisspelledWord(&hasMisspelledWord)); + if (!hasMisspelledWord) + return S_OK; + + // Take deferral — menu will be shown after async callback. + wil::com_ptr deferral; + CHECK_FAILURE(args->GetDeferral(&deferral)); + CHECK_FAILURE(args->put_Handled(true)); + + // Asynchronously retrieve spell check suggestions. + CHECK_FAILURE(target2->GetSpellCheckSuggestions( + Callback< + ICoreWebView2GetSpellCheckSuggestionsCompletedHandler>( + [args, deferral]( + HRESULT errorCode, + ICoreWebView2ContextMenuItemCollection* + suggestions) -> HRESULT + { + // Enumerate suggestions — each has Label and CommandId. + UINT32 count = 0; + if (SUCCEEDED(errorCode) && suggestions) + suggestions->get_Count(&count); + + for (UINT32 i = 0; i < count; i++) + { + wil::com_ptr item; + suggestions->GetValueAtIndex(i, &item); + wil::unique_cotaskmem_string label; + item->get_Label(&label); + INT32 cmdId; + item->get_CommandId(&cmdId); + // ... add to custom menu using label and cmdId ... + } + + // Apply selection via unified commanding. + // args->put_SelectedCommandId(selectedCmdId); + + deferral->Complete(); + return S_OK; + }) + .Get())); + return S_OK; + }) + .Get(), + &m_contextMenuRequestedToken); +``` + +## .NET/WinRT + +```csharp +webView.CoreWebView2.ContextMenuRequested += async (sender, args) => +{ + var target = args.ContextMenuTarget; + + // Check if the context menu target has a misspelled word. + if (!target.HasMisspelledWord) + return; + + // Take deferral — menu will be shown after async call completes. + var deferral = args.GetDeferral(); + args.Handled = true; + + // Asynchronously retrieve spell check suggestions. + IReadOnlyList suggestions = + await target.GetSpellCheckSuggestionsAsync(); + + // Build custom menu with suggestions. + var contextMenu = new ContextMenuStrip(); + foreach (var suggestion in suggestions) + { + var item = new ToolStripMenuItem(suggestion.Label); + var capturedId = suggestion.CommandId; + item.Click += (_, _) => + { + // Apply selection via unified commanding. + args.SelectedCommandId = capturedId; + }; + contextMenu.Items.Add(item); + } + + // Show menu and complete deferral when closed. + contextMenu.Closed += (_, _) => deferral.Complete(); + contextMenu.Show(webView, new Point(args.Location.X, args.Location.Y)); +}; +``` + +# API Details + +## Win32 COM IDL + +```idl +// ─── ContextMenuTarget2: Spell check support ─── + +/// Extends `ICoreWebView2ContextMenuTarget` with spell check support for +/// custom context menus. +/// +/// The host can `QueryInterface` the `ICoreWebView2ContextMenuTarget` returned +/// by `ICoreWebView2ContextMenuRequestedEventArgs::get_ContextMenuTarget` to +/// obtain this interface. Check `HasMisspelledWord` to determine whether +/// the context menu was invoked on a misspelled word, then call +/// `GetSpellCheckSuggestions` to asynchronously retrieve spelling corrections. +/// +/// To apply a suggestion, pass the selected item's `CommandId` to +/// `ICoreWebView2ContextMenuRequestedEventArgs::put_SelectedCommandId`. +[uuid(f7a3b8c1-2d4e-5f6a-8b9c-0d1e2f3a4b5c), object, pointer_default(unique)] +interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { + /// Returns TRUE if the context menu target contains a misspelled word. + /// When TRUE, call `GetSpellCheckSuggestions` to retrieve the available + /// spelling correction suggestions asynchronously. + [propget] HRESULT HasMisspelledWord([out, retval] BOOL* value); + + /// Asynchronously retrieves spell check suggestion options as a collection + /// of context menu items. The handler is invoked immediately if suggestions + /// are already available, or when they become available from the platform + /// spell check engine. Each item's `Label` is the suggestion text and its + /// `CommandId` can be passed to `put_SelectedCommandId` to apply the + /// correction. The handler receives an empty collection if no suggestions + /// are available, if `HasMisspelledWord` is FALSE, or if the underlying + /// spell check service does not respond within an internal timeout. + /// Multiple concurrent calls are supported; each handler will be invoked + /// with the same result when suggestions become available. + /// Returns `E_POINTER` if `handler` is null. + HRESULT GetSpellCheckSuggestions( + [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); +} + +/// Receives the result of the `GetSpellCheckSuggestions` method. +[uuid(d73832f9-d05b-438d-bb6d-6441245221e3), object, pointer_default(unique)] +interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { + /// Provides the result of the corresponding asynchronous method. + /// Each item in the `suggestions` collection is an + /// `ICoreWebView2ContextMenuItem` whose `Label` is the suggestion text + /// and whose `CommandId` uniquely identifies it. To apply a suggestion, + /// pass the selected item's `CommandId` to + /// `ICoreWebView2ContextMenuRequestedEventArgs.put_SelectedCommandId`. + HRESULT Invoke( + [in] HRESULT errorCode, + [in] ICoreWebView2ContextMenuItemCollection* suggestions); +} +``` + +## .NET/WinRT + +```csharp +namespace Microsoft.Web.WebView2.Core +{ + runtimeclass CoreWebView2ContextMenuTarget + { + // Existing members unchanged. + + [interface_name("ICoreWebView2ContextMenuTarget2")] + { + /// + /// Returns TRUE if the context menu target contains a misspelled word. + /// + Boolean HasMisspelledWord { get; }; + + /// + /// Asynchronously retrieves spell check suggestions. Each item's + /// CommandId can be passed to SelectedCommandId to apply the correction. + /// + Windows.Foundation.IAsyncOperation> + GetSpellCheckSuggestionsAsync(); + } + } +} +``` + +# Behavioral Details + +## Discovery Flow + +| Step | Action | Result | +|------|--------|--------| +| 1 | QI for `Target2` from `ContextMenuTarget` | `E_NOINTERFACE` → old runtime, fall back to default menu | +| 2 | Read `HasMisspelledWord` | `TRUE` → misspelling present; `FALSE` → no misspelling | +| 3 | Call `GetSpellCheckSuggestions(handler)` | Handler invoked when suggestions are available | + +## Suggestion Item Properties + +Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestions` has: + +| Property | Value | +|----------|-------| +| `Label` | Suggestion text (e.g., "the") | +| `CommandId` | WebView2-allocated opaque ID (e.g., 50001) | +| `Name` | `"spellCheckSuggestion"` | +| `Kind` | `COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND` | +| `IsEnabled` | true | +| `IsChecked` | false | +| `Icon` | null | +| `ShortcutKeyDescription` | empty string | +| `Children` | null | + +## Async Timing + +Spell check suggestions are resolved asynchronously by the platform spell checker in the browser +process. When `ContextMenuRequested` fires, the suggestions may be: + +| State | Meaning | `GetSpellCheckSuggestions` behavior | +|-------|---------|-------------------------------------| +| **Ready** | Suggestions already resolved before the event fired | Handler invoked immediately | +| **Not Ready** | Platform spell checker still working | Handler stored; invoked when browser delivers results via IPC, or after internal timeout with empty collection | + +The host does **not** need to check readiness — `GetSpellCheckSuggestions` handles both cases +transparently. In the typical case, the platform spell checker responds within a few milliseconds. +The internal timeout is a conservative safeguard for rare scenarios where the platform spell checker +is slow or unresponsive. + +### Host Patterns + +**Pattern 1: Wait-then-show** (simpler — used in the examples above) + +The host defers the context menu, calls `GetSpellCheckSuggestions`, and builds/shows the menu +only after the handler fires. This produces a complete menu in one shot but delays appearance +if suggestions are not yet ready. + +``` +ContextMenuRequested → put_Handled(TRUE) + GetDeferral → GetSpellCheckSuggestions + → [handler fires] → build & show menu → complete deferral +``` + +**Pattern 2: Show-then-update** (responsive — mirrors browser built-in behavior) + +The host shows the context menu immediately with a placeholder (e.g., "Loading suggestions…") +and updates it in-place when the handler fires. This keeps menu appearance instant at the cost +of added complexity. Since the host owns the custom context menu UI, it can modify the menu +while it is open. + +``` +ContextMenuRequested → put_Handled(TRUE) + GetDeferral → show menu with placeholder + → GetSpellCheckSuggestions → [handler fires] → update menu items in-place + → [user selects] → complete deferral +``` + +Either pattern is valid. Pattern 1 is recommended for most hosts because the delay is typically +imperceptible (suggestions often resolve before the event fires or within a few milliseconds +after). Pattern 2 is appropriate for hosts that require guaranteed instant menu appearance. + +# Appendix + +## Planned Spell Check Extensions + +The following actions will be added as additional `ICoreWebView2ContextMenuItem` entries in the +collection returned by `GetSpellCheckSuggestions`. No new interfaces or methods are required: + +| Action | `Name` value | +|--------|-------------| +| Add to Dictionary | `"spellCheckAddToDictionary"` | +| Ignore (session) | `"spellCheckIgnore"` | + +These follow the same commanding model: the host renders them like any other item and applies via +`SelectedCommandId`. A `Language` property (BCP-47 tag of the dictionary that flagged the misspelling) +may also be added to `ICoreWebView2ContextMenuTarget2` in a follow-up version. Profile-level +spell check configuration (`IsSpellCheckEnabled`, `SpellCheckLanguages`) is tracked as a separate +follow-up. + +## Relationship to Existing APIs + +| Existing API | This Feature | +|-------------|-------------| +| `EventArgs.MenuItems` | Synchronous snapshot of menu items | +| `EventArgs.SelectedCommandId` | Execution path — now also used for spell check suggestions | +| `ContextMenuItem.CommandId` | Already used for all items — spell check items join this pool | +| `ContextMenuItem.Label` | Display text — spell check suggestions use this for the suggestion word | +| `EventArgs.GetDeferral()` | Must be held across the async `GetSpellCheckSuggestions` gap | +| `ContextMenuTarget` | Base target — QI to `Target2` for spell check support |