source: trunk/src/ole32/oleMenu.cpp@ 851

Last change on this file since 851 was 851, checked in by davidr, 26 years ago

Initial port of OLE2.
Added regsvr32

File size: 19.2 KB
Line 
1/*
2 *
3 * Project Odin Software License can be found in LICENSE.TXT
4 *
5 */
6/*
7 * OLEMENU functions.
8 *
9 * 4/9/99
10 *
11 * Copyright 1999 David J. Raison
12 *
13 * Some portions from Wine Implementation (2/9/99)
14 * Copyright 1995 Martin von Loewis
15 * Copyright 1999 Francis Beaudet
16 * Copyright 1999 Noel Borthwick
17 */
18
19#include "ole32.h"
20#include "commctrl.h"
21#include "oString.h"
22#include <assert.h>
23
24// ======================================================================
25// Local Data
26// ======================================================================
27
28typedef struct tagOleMenuDescriptor /* OleMenuDescriptor */
29{
30 HWND hwndFrame; /* The containers frame window */
31 HWND hwndActiveObject; /* The active objects window */
32 OLEMENUGROUPWIDTHS mgw; /* OLE menu group widths for the shared menu */
33 HMENU hmenuCombined; /* The combined menu */
34 BOOL bIsServerItem; /* True if the currently open popup belongs to the server */
35} OleMenuDescriptor;
36
37typedef struct tagOleMenuHookItem /* OleMenu hook item in per thread hook list */
38{
39 DWORD tid; /* Thread Id */
40 HANDLE hHeap; /* Heap this is allocated from */
41 HHOOK GetMsg_hHook; /* message hook for WH_GETMESSAGE */
42 HHOOK CallWndProc_hHook; /* message hook for WH_CALLWNDPROC */
43} OleMenuHookItem;
44
45/*
46 * Dynamic pointer array of per thread message hooks (maintained by OleSetMenuDescriptor)
47 */
48static HDPA OLEMenu_MsgHookDPA = NULL;
49
50
51// ======================================================================
52// Prototypes.
53// ======================================================================
54static void OLEUTL_ReadRegistryDWORDValue(HKEY regKey, DWORD* pdwValue);
55
56// These are the prototypes of the utility methods used to manage a shared menu
57extern void OLEMenu_Initialize();
58extern void OLEMenu_UnInitialize();
59
60BOOL OLEMenu_InstallHooks( DWORD tid );
61BOOL OLEMenu_UnInstallHooks( DWORD tid );
62OleMenuHookItem * OLEMenu_IsHookInstalled( DWORD tid, INT *pixHook );
63static BOOL OLEMenu_FindMainMenuIndex( HMENU hMainMenu, HMENU hPopupMenu, UINT *pnPos );
64BOOL OLEMenu_SetIsServerMenu( HMENU hmenu, OleMenuDescriptor *pOleMenuDescriptor );
65LRESULT CALLBACK OLEMenu_CallWndProc(INT code, WPARAM wParam, LPARAM lParam);
66LRESULT CALLBACK OLEMenu_GetMsgProc(INT code, WPARAM wParam, LPARAM lParam);
67
68// ======================================================================
69// Public API's
70// ======================================================================
71
72/*
73 * OleCreateMenuDescriptor [OLE32.97]
74 * Creates an OLE menu descriptor for OLE to use when dispatching
75 * menu messages and commands.
76 *
77 * PARAMS:
78 * hmenuCombined - Handle to the objects combined menu
79 * lpMenuWidths - Pointer to array of 6 LONG's indicating menus per group
80 *
81 */
82HOLEMENU WIN32API OleCreateMenuDescriptor(
83 HMENU hmenuCombined,
84 LPOLEMENUGROUPWIDTHS lpMenuWidths)
85{
86 HOLEMENU hOleMenu;
87 OleMenuDescriptor *pOleMenuDescriptor;
88 int i;
89
90 if ( !hmenuCombined || !lpMenuWidths )
91 return 0;
92
93 /* Create an OLE menu descriptor */
94 if ( !(hOleMenu = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT,
95 sizeof(OleMenuDescriptor) ) ) )
96 return 0;
97
98 pOleMenuDescriptor = (OleMenuDescriptor *) GlobalLock( hOleMenu );
99 if ( !pOleMenuDescriptor )
100 return 0;
101
102 /* Initialize menu group widths and hmenu */
103 for ( i = 0; i < 6; i++ )
104 pOleMenuDescriptor->mgw.width[i] = lpMenuWidths->width[i];
105
106 pOleMenuDescriptor->hmenuCombined = hmenuCombined;
107 pOleMenuDescriptor->bIsServerItem = FALSE;
108 GlobalUnlock( hOleMenu );
109
110 return hOleMenu;
111}
112
113/*
114 * OleDestroyMenuDescriptor [OLE32.99]
115 * Destroy the shared menu descriptor
116 */
117HRESULT WIN32API OleDestroyMenuDescriptor(
118 HOLEMENU hmenuDescriptor)
119{
120 if ( hmenuDescriptor )
121 GlobalFree( hmenuDescriptor );
122 return S_OK;
123}
124
125/*
126 * OleSetMenuDescriptor [OLE32.129]
127 * Installs or removes OLE dispatching code for the containers frame window
128 * FIXME: The lpFrame and lpActiveObject parameters are currently ignored
129 * OLE should install context sensitive help F1 filtering for the app when
130 * these are non null.
131 *
132 * PARAMS:
133 * hOleMenu Handle to composite menu descriptor
134 * hwndFrame Handle to containers frame window
135 * hwndActiveObject Handle to objects in-place activation window
136 * lpFrame Pointer to IOleInPlaceFrame on containers window
137 * lpActiveObject Pointer to IOleInPlaceActiveObject on active in-place object
138 *
139 * RETURNS:
140 * S_OK - menu installed correctly
141 * E_FAIL, E_INVALIDARG, E_UNEXPECTED - failure
142 */
143HRESULT WIN32API OleSetMenuDescriptor(
144 HOLEMENU hOleMenu,
145 HWND hwndFrame,
146 HWND hwndActiveObject,
147 LPOLEINPLACEFRAME lpFrame,
148 LPOLEINPLACEACTIVEOBJECT lpActiveObject)
149{
150 OleMenuDescriptor *pOleMenuDescriptor = NULL;
151
152 /* Check args */
153 if ( !hwndFrame || (hOleMenu && !hwndActiveObject) )
154 return E_INVALIDARG;
155
156 if ( lpFrame || lpActiveObject )
157 {
158 dprintf(("OLE32: OleSetMenuDescriptor(%x, %x, %x, %p, %p), Context sensitive help filtering not implemented!\n",
159 (unsigned int)hOleMenu,
160 hwndFrame,
161 hwndActiveObject,
162 lpFrame,
163 lpActiveObject));
164 }
165
166 /* Set up a message hook to intercept the containers frame window messages.
167 * The message filter is responsible for dispatching menu messages from the
168 * shared menu which are intended for the object.
169 */
170
171 if ( hOleMenu ) /* Want to install dispatching code */
172 {
173 /* If OLEMenu hooks are already installed for this thread, fail
174 * Note: This effectively means that OleSetMenuDescriptor cannot
175 * be called twice in succession on the same frame window
176 * without first calling it with a null hOleMenu to uninstall */
177 if ( OLEMenu_IsHookInstalled( GetCurrentThreadId(), NULL ) )
178 return E_FAIL;
179
180 /* Get the menu descriptor */
181 pOleMenuDescriptor = (OleMenuDescriptor *) GlobalLock( hOleMenu );
182 if ( !pOleMenuDescriptor )
183 return E_UNEXPECTED;
184
185 /* Update the menu descriptor */
186 pOleMenuDescriptor->hwndFrame = hwndFrame;
187 pOleMenuDescriptor->hwndActiveObject = hwndActiveObject;
188
189 GlobalUnlock( hOleMenu );
190 pOleMenuDescriptor = NULL;
191
192 /* Add a menu descriptor windows property to the frame window */
193 SetPropA( hwndFrame, "PROP_OLEMenuDescriptor", hOleMenu );
194
195 /* Install thread scope message hooks for WH_GETMESSAGE and WH_CALLWNDPROC */
196 if ( !OLEMenu_InstallHooks( GetCurrentThreadId() ) )
197 return E_FAIL;
198 }
199 else /* Want to uninstall dispatching code */
200 {
201 /* Uninstall the hooks */
202 if ( !OLEMenu_UnInstallHooks( GetCurrentThreadId() ) )
203 return E_FAIL;
204
205 /* Remove the menu descriptor property from the frame window */
206 RemovePropA( hwndFrame, "PROP_OLEMenuDescriptor" );
207 }
208
209 return S_OK;
210}
211
212/*
213 * OLEMenu_Initialize()
214 *
215 * Initializes the OLEMENU data structures.
216 */
217extern void OLEMenu_Initialize()
218{
219 dprintf(("OLE32: OLEMenu_Initialize"));
220
221 /* Create a dynamic pointer array to store the hook handles */
222 if ( !OLEMenu_MsgHookDPA )
223 OLEMenu_MsgHookDPA = DPA_CreateEx( 2, GetProcessHeap() );
224}
225
226/*
227 * OLEMenu_UnInitialize()
228 *
229 * Releases the OLEMENU data structures.
230 */
231extern void OLEMenu_UnInitialize()
232{
233 dprintf(("OLE32: OLEMenu_UnInitialize"));
234
235 /* Release the hook table */
236 if ( OLEMenu_MsgHookDPA )
237 DPA_Destroy( OLEMenu_MsgHookDPA );
238
239 OLEMenu_MsgHookDPA = NULL;
240}
241
242// ======================================================================
243// Private functions.
244// ======================================================================
245
246/*
247 * Internal methods to manage the shared OLE menu in response to the
248 * OLE***MenuDescriptor API
249 */
250
251/*
252 * OLEMenu_InstallHooks
253 * Install thread scope message hooks for WH_GETMESSAGE and WH_CALLWNDPROC
254 *
255 * RETURNS: TRUE if message hooks were succesfully installed
256 * FALSE on failure
257 */
258BOOL OLEMenu_InstallHooks( DWORD tid )
259{
260 OleMenuHookItem *pHookItem = NULL;
261
262 if ( !OLEMenu_MsgHookDPA ) /* No hook table? Create one */
263 {
264 /* Create a dynamic pointer array to store the hook handles */
265 if ( !(OLEMenu_MsgHookDPA = DPA_CreateEx( 2, GetProcessHeap() )) )
266 return FALSE;
267 }
268
269 /* Create an entry for the hook table */
270 if ( !(pHookItem = (OleMenuHookItem *)HeapAlloc(GetProcessHeap(), 0,
271 sizeof(OleMenuHookItem)) ) )
272 return FALSE;
273
274 pHookItem->tid = tid;
275 pHookItem->hHeap = GetProcessHeap();
276
277 /* Install a thread scope message hook for WH_GETMESSAGE */
278 pHookItem->GetMsg_hHook = SetWindowsHookExA( WH_GETMESSAGE, OLEMenu_GetMsgProc,
279 0, GetCurrentThreadId() );
280 if ( !pHookItem->GetMsg_hHook )
281 goto CLEANUP;
282
283 /* Install a thread scope message hook for WH_CALLWNDPROC */
284 pHookItem->CallWndProc_hHook = SetWindowsHookExA( WH_CALLWNDPROC, OLEMenu_CallWndProc,
285 0, GetCurrentThreadId() );
286 if ( !pHookItem->CallWndProc_hHook )
287 goto CLEANUP;
288
289 /* Insert the hook table entry */
290 if ( -1 == DPA_InsertPtr( OLEMenu_MsgHookDPA, 0, pHookItem ) )
291 goto CLEANUP;
292
293 return TRUE;
294
295CLEANUP:
296 /* Unhook any hooks */
297 if ( pHookItem->GetMsg_hHook )
298 UnhookWindowsHookEx( pHookItem->GetMsg_hHook );
299 if ( pHookItem->CallWndProc_hHook )
300 UnhookWindowsHookEx( pHookItem->CallWndProc_hHook );
301 /* Release the hook table entry */
302 HeapFree(pHookItem->hHeap, 0, pHookItem );
303
304 return FALSE;
305}
306
307/*
308 * OLEMenu_UnInstallHooks
309 * UnInstall thread scope message hooks for WH_GETMESSAGE and WH_CALLWNDPROC
310 *
311 * RETURNS: TRUE if message hooks were succesfully installed
312 * FALSE on failure
313 */
314BOOL OLEMenu_UnInstallHooks( DWORD tid )
315{
316 INT ixHook;
317 OleMenuHookItem *pHookItem = NULL;
318
319 if ( !OLEMenu_MsgHookDPA ) /* No hooks set */
320 return TRUE;
321
322 /* Lookup the hHook index for this tid */
323 if ( !OLEMenu_IsHookInstalled( tid , &ixHook ) )
324 return TRUE;
325
326 /* Remove the hook entry from the table(the pointer itself is not deleted) */
327 if ( !( pHookItem = (OleMenuHookItem *)DPA_DeletePtr(OLEMenu_MsgHookDPA, ixHook) ) )
328 return FALSE;
329
330 /* Uninstall the hooks installed for this thread */
331 if ( !UnhookWindowsHookEx( pHookItem->GetMsg_hHook ) )
332 goto CLEANUP;
333 if ( !UnhookWindowsHookEx( pHookItem->CallWndProc_hHook ) )
334 goto CLEANUP;
335
336 /* Release the hook table entry */
337 HeapFree(pHookItem->hHeap, 0, pHookItem );
338
339 return TRUE;
340
341CLEANUP:
342 /* Release the hook table entry */
343 if (pHookItem)
344 HeapFree(pHookItem->hHeap, 0, pHookItem );
345
346 return FALSE;
347}
348
349/*
350 * OLEMenu_IsHookInstalled
351 * Tests if OLEMenu hooks have been installed for a thread
352 *
353 * RETURNS: The pointer and index of the hook table entry for the tid
354 * NULL and -1 for the index if no hooks were installed for this thread
355 */
356OleMenuHookItem * OLEMenu_IsHookInstalled( DWORD tid, INT *pixHook )
357{
358 INT ixHook;
359 OleMenuHookItem *pHookItem = NULL;
360
361 if ( pixHook )
362 *pixHook = -1;
363
364 if ( !OLEMenu_MsgHookDPA ) /* No hooks set */
365 return NULL;
366
367 /* Do a simple linear search for an entry whose tid matches ours.
368 * We really need a map but efficiency is not a concern here. */
369 for( ixHook = 0; ; ixHook++ )
370 {
371 /* Retrieve the hook entry */
372 if ( !( pHookItem = (OleMenuHookItem *)DPA_GetPtr(OLEMenu_MsgHookDPA, ixHook) ) )
373 return NULL;
374
375 if ( tid == pHookItem->tid )
376 {
377 if ( pixHook )
378 *pixHook = ixHook;
379 return pHookItem;
380 }
381 }
382
383 return NULL;
384}
385
386/*
387 * OLEMenu_FindMainMenuIndex
388 *
389 * Used by OLEMenu API to find the top level group a menu item belongs to.
390 * On success pnPos contains the index of the item in the top level menu group
391 *
392 * RETURNS: TRUE if the ID was found, FALSE on failure
393 */
394static BOOL OLEMenu_FindMainMenuIndex( HMENU hMainMenu, HMENU hPopupMenu, UINT *pnPos )
395{
396 UINT i, nItems;
397
398 nItems = GetMenuItemCount( hMainMenu );
399
400 for (i = 0; i < nItems; i++)
401 {
402 HMENU hsubmenu;
403
404 /* Is the current item a submenu? */
405 if ( (hsubmenu = GetSubMenu(hMainMenu, i)) != NULL)
406 {
407 /* If the handle is the same we're done */
408 if ( hsubmenu == hPopupMenu )
409 {
410 if (pnPos)
411 *pnPos = i;
412 return TRUE;
413 }
414 /* Recursively search without updating pnPos */
415 else if ( OLEMenu_FindMainMenuIndex( hsubmenu, hPopupMenu, NULL ) )
416 {
417 if (pnPos)
418 *pnPos = i;
419 return TRUE;
420 }
421 }
422 }
423
424 return FALSE;
425}
426
427/*
428 * OLEMenu_SetIsServerMenu
429 *
430 * Checks whether a popup menu belongs to a shared menu group which is
431 * owned by the server, and sets the menu descriptor state accordingly.
432 * All menu messages from these groups should be routed to the server.
433 *
434 * RETURNS: TRUE if the popup menu is part of a server owned group
435 * FASE if the popup menu is part of a container owned group
436 */
437BOOL OLEMenu_SetIsServerMenu( HMENU hmenu, OleMenuDescriptor *pOleMenuDescriptor )
438{
439 UINT nPos = 0, nWidth, i;
440
441 pOleMenuDescriptor->bIsServerItem = FALSE;
442
443 /* Don't bother searching if the popup is the combined menu itself */
444 if ( hmenu == pOleMenuDescriptor->hmenuCombined )
445 return FALSE;
446
447 /* Find the menu item index in the shared OLE menu that this item belongs to */
448 if ( !OLEMenu_FindMainMenuIndex( pOleMenuDescriptor->hmenuCombined, hmenu, &nPos ) )
449 return FALSE;
450
451 /* The group widths array has counts for the number of elements
452 * in the groups File, Edit, Container, Object, Window, Help.
453 * The Edit, Object & Help groups belong to the server object
454 * and the other three belong to the container.
455 * Loop thru the group widths and locate the group we are a member of.
456 */
457 for ( i = 0, nWidth = 0; i < 6; i++ )
458 {
459 nWidth += pOleMenuDescriptor->mgw.width[i];
460 if ( nPos < nWidth )
461 {
462 /* Odd elements are server menu widths */
463 pOleMenuDescriptor->bIsServerItem = (i%2) ? TRUE : FALSE;
464 break;
465 }
466 }
467
468 return pOleMenuDescriptor->bIsServerItem;
469}
470
471/*
472 * OLEMenu_CallWndProc
473 * Thread scope WH_CALLWNDPROC hook proc filter function (callback)
474 * This is invoked from a message hook installed in OleSetMenuDescriptor.
475 */
476LRESULT CALLBACK OLEMenu_CallWndProc(INT code, WPARAM wParam, LPARAM lParam)
477{
478 LPCWPSTRUCT pMsg = NULL;
479 HOLEMENU hOleMenu = 0;
480 OleMenuDescriptor *pOleMenuDescriptor = NULL;
481 OleMenuHookItem *pHookItem = NULL;
482 WORD fuFlags;
483
484 dprintf(("OLE32: OLEMenu_CallWndProc %i, %04x, %08x", code, wParam, (unsigned)lParam));
485
486 /* Check if we're being asked to process the message */
487 if ( HC_ACTION != code )
488 goto NEXTHOOK;
489
490 /* Retrieve the current message being dispatched from lParam */
491 pMsg = (LPCWPSTRUCT)lParam;
492
493 /* Check if the message is destined for a window we are interested in:
494 * If the window has an OLEMenu property we may need to dispatch
495 * the menu message to its active objects window instead. */
496
497 hOleMenu = (HOLEMENU)GetPropA( pMsg->hwnd, "PROP_OLEMenuDescriptor" );
498 if ( !hOleMenu )
499 goto NEXTHOOK;
500
501 /* Get the menu descriptor */
502 pOleMenuDescriptor = (OleMenuDescriptor *) GlobalLock( hOleMenu );
503 if ( !pOleMenuDescriptor ) /* Bad descriptor! */
504 goto NEXTHOOK;
505
506 /* Process menu messages */
507 switch( pMsg->message )
508 {
509 case WM_INITMENU:
510 {
511 /* Reset the menu descriptor state */
512 pOleMenuDescriptor->bIsServerItem = FALSE;
513
514 /* Send this message to the server as well */
515 SendMessageA( pOleMenuDescriptor->hwndActiveObject,
516 pMsg->message, pMsg->wParam, pMsg->lParam );
517 goto NEXTHOOK;
518 }
519
520 case WM_INITMENUPOPUP:
521 {
522 /* Save the state for whether this is a server owned menu */
523 OLEMenu_SetIsServerMenu( (HMENU)pMsg->wParam, pOleMenuDescriptor );
524 break;
525 }
526
527 case WM_MENUSELECT:
528 {
529 fuFlags = HIWORD(pMsg->wParam); /* Get flags */
530 if ( fuFlags & MF_SYSMENU )
531 goto NEXTHOOK;
532
533 /* Save the state for whether this is a server owned popup menu */
534 else if ( fuFlags & MF_POPUP )
535 OLEMenu_SetIsServerMenu( (HMENU)pMsg->lParam, pOleMenuDescriptor );
536
537 break;
538 }
539
540 case WM_DRAWITEM:
541 {
542 LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT) pMsg->lParam;
543 if ( pMsg->wParam != 0 || lpdis->CtlType != ODT_MENU )
544 goto NEXTHOOK; /* Not a menu message */
545
546 break;
547 }
548
549 default:
550 goto NEXTHOOK;
551 }
552
553 /* If the message was for the server dispatch it accordingly */
554 if ( pOleMenuDescriptor->bIsServerItem )
555 {
556 SendMessageA( pOleMenuDescriptor->hwndActiveObject,
557 pMsg->message, pMsg->wParam, pMsg->lParam );
558 }
559
560NEXTHOOK:
561 if ( pOleMenuDescriptor )
562 GlobalUnlock( hOleMenu );
563
564 /* Lookup the hook item for the current thread */
565 if ( !( pHookItem = OLEMenu_IsHookInstalled( GetCurrentThreadId(), NULL ) ) )
566 {
567 /* This should never fail!! */
568 dprintf(("Warning: Could not retrieve hHook for current thread!"));
569 return 0;
570 }
571
572 /* Pass on the message to the next hooker */
573 return CallNextHookEx( pHookItem->CallWndProc_hHook, code, wParam, lParam );
574}
575
576/*
577 * OLEMenu_GetMsgProc
578 * Thread scope WH_GETMESSAGE hook proc filter function (callback)
579 * This is invoked from a message hook installed in OleSetMenuDescriptor.
580 */
581LRESULT CALLBACK OLEMenu_GetMsgProc(INT code, WPARAM wParam, LPARAM lParam)
582{
583 LPMSG pMsg = NULL;
584 HOLEMENU hOleMenu = 0;
585 OleMenuDescriptor *pOleMenuDescriptor = NULL;
586 OleMenuHookItem *pHookItem = NULL;
587 WORD wCode;
588
589 dprintf(("OLE32: OLEMenu_GetMsgProc %i, %04x, %08x", code, wParam, (unsigned)lParam ));
590
591 /* Check if we're being asked to process a messages */
592 if ( HC_ACTION != code )
593 goto NEXTHOOK;
594
595 /* Retrieve the current message being dispatched from lParam */
596 pMsg = (LPMSG)lParam;
597
598 /* Check if the message is destined for a window we are interested in:
599 * If the window has an OLEMenu property we may need to dispatch
600 * the menu message to its active objects window instead. */
601
602 hOleMenu = (HOLEMENU)GetPropA( pMsg->hwnd, "PROP_OLEMenuDescriptor" );
603 if ( !hOleMenu )
604 goto NEXTHOOK;
605
606 /* Process menu messages */
607 switch( pMsg->message )
608 {
609 case WM_COMMAND:
610 {
611 wCode = HIWORD(pMsg->wParam); /* Get notification code */
612 if ( wCode )
613 goto NEXTHOOK; /* Not a menu message */
614 break;
615 }
616 default:
617 goto NEXTHOOK;
618 }
619
620 /* Get the menu descriptor */
621 pOleMenuDescriptor = (OleMenuDescriptor *) GlobalLock( hOleMenu );
622 if ( !pOleMenuDescriptor ) /* Bad descriptor! */
623 goto NEXTHOOK;
624
625 /* If the message was for the server dispatch it accordingly */
626 if ( pOleMenuDescriptor->bIsServerItem )
627 {
628 /* Change the hWnd in the message to the active objects hWnd.
629 * The message loop which reads this message will automatically
630 * dispatch it to the embedded objects window. */
631 pMsg->hwnd = pOleMenuDescriptor->hwndActiveObject;
632 }
633
634NEXTHOOK:
635 if ( pOleMenuDescriptor )
636 GlobalUnlock( hOleMenu );
637
638 /* Lookup the hook item for the current thread */
639 if ( !( pHookItem = OLEMenu_IsHookInstalled( GetCurrentThreadId(), NULL ) ) )
640 {
641 /* This should never fail!! */
642 dprintf(("Warning: Could not retrieve hHook for current thread!\n" ));
643 return FALSE;
644 }
645
646 /* Pass on the message to the next hooker */
647 return CallNextHookEx( pHookItem->GetMsg_hHook, code, wParam, lParam );
648}
649
Note: See TracBrowser for help on using the repository browser.