Skip to content

Commit 417e474

Browse files
author
Anurag Kumar
committed
initial api design
1 parent e79867c commit 417e474

1 file changed

Lines changed: 381 additions & 0 deletions

File tree

specs/custommenuspellcheck.md

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
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

Comments
 (0)