Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

626 lines
19 KiB

  1. #include "shellprv.h"
  2. #include "clsobj.h"
  3. #include "resource.h"
  4. #include "caggunk.h"
  5. #include "menuisf.h"
  6. #include "menubar.h"
  7. #include "menuband.h"
  8. #include "iaccess.h"
  9. #include "apithk.h"
  10. //=================================================================
  11. // Implementation of CMenuAgent
  12. //
  13. // The single global object of this class (g_menuagent) is the
  14. // manager of the message filter proc used to track mouse and
  15. // keyboard messages on behalf of CTrackPopupBar while a menu is
  16. // in a modal menu loop in TrackPopupMenu.
  17. //
  18. // We track these messages so we can pop out of the menu, behaving
  19. // as if the visual menu bar consisted of a homogeneous menu
  20. // object.
  21. //
  22. //=================================================================
  23. extern "C" void DumpMsg(LPCTSTR pszLabel, MSG * pmsg);
  24. struct CMenuAgent
  25. {
  26. public:
  27. HHOOK _hhookMsg;
  28. HWND _hwndSite; // hwnd to receive forwarded messages
  29. HWND _hwndParent;
  30. CTrackPopupBar * _ptpbar;
  31. IMenuPopup * _pmpParent;
  32. void* _pvContext;
  33. BITBOOL _fEscHit: 1;
  34. // we need to keep track of whether the last selected
  35. // menu item was on a popup or not. we can do this by storing the
  36. // last WM_MENUSELECT flags
  37. UINT _uFlagsLastSelected;
  38. HMENU _hmenuLastSelected;
  39. POINT _ptLastMove;
  40. void Init(void* pvContext, CTrackPopupBar * ptpbar, IMenuPopup * pmpParent, HWND hwndParent, HWND hwndSite);
  41. void Reset(void* pvContext);
  42. void CancelMenu(void* pvContext);
  43. static LRESULT CALLBACK MsgHook(int nCode, WPARAM wParam, LPARAM lParam);
  44. //private:
  45. void _OnMenuSelect(HMENU hmenu, int i, UINT uFlags);
  46. BOOL _OnKey(WPARAM vkey);
  47. };
  48. // Just one of these, b/c we only need one message filter
  49. CMenuAgent g_menuagent = { 0 };
  50. /*----------------------------------------------------------
  51. Purpose: Initialize the message filter hook
  52. */
  53. void CMenuAgent::Init(void* pvContext, CTrackPopupBar * ptpbar, IMenuPopup * pmpParent,
  54. HWND hwndParent, HWND hwndSite)
  55. {
  56. TraceMsg(TF_MENUBAND, "Initialize CMenuAgent");
  57. ASSERT(IS_VALID_READ_PTR(ptpbar, CTrackPopupBar));
  58. ASSERT(IS_VALID_CODE_PTR(pmpParent, IMenuPopup));
  59. ASSERT(IS_VALID_HANDLE(hwndSite, WND));
  60. if (_pvContext != pvContext)
  61. {
  62. // When switching contexts, we need to collapse the old menu. This keeps us from
  63. // hosing the menubands when switching from one browser to another.
  64. CancelMenu(_pvContext);
  65. ATOMICRELEASE(_ptpbar);
  66. ATOMICRELEASE(_pmpParent);
  67. _pvContext = pvContext;
  68. }
  69. pmpParent->SetSubMenu(ptpbar, TRUE);
  70. _hwndSite = hwndSite;
  71. _hwndParent = hwndParent;
  72. // Since the message hook wants to forward messages to the toolbar,
  73. // we need to ask the pager control to do this
  74. Pager_ForwardMouse(_hwndSite, TRUE);
  75. _pmpParent = pmpParent;
  76. _pmpParent->AddRef();
  77. _ptpbar = ptpbar;
  78. _ptpbar->AddRef();
  79. if (NULL == _hhookMsg)
  80. {
  81. _hhookMsg = SetWindowsHookEx(WH_MSGFILTER, MsgHook, HINST_THISDLL, 0);
  82. }
  83. _fEscHit = FALSE;
  84. GetCursorPos(&_ptLastMove);
  85. }
  86. /*----------------------------------------------------------
  87. Purpose: Reset the menu agent; no longer track mouse and keyboard
  88. messages. The menuband calls this when it exits menu mode.
  89. */
  90. void CMenuAgent::Reset(void* pvContext)
  91. {
  92. if (_pvContext == pvContext)
  93. {
  94. _pmpParent->SetSubMenu(_ptpbar, FALSE);
  95. // The only time to not send MPOS_FULLCANCEL is if the escape
  96. // key caused the menu to terminate.
  97. if ( !_fEscHit )
  98. _pmpParent->OnSelect(MPOS_FULLCANCEL);
  99. // Eat any mouse-down/up sequence left in the queue. This is how
  100. // we keep the toolbar from getting a mouse-down if the user
  101. // clicks on the same menuitem as what is currently popped down.
  102. // (E.g., click File, then click File again. W/o this, the menu
  103. // would never toggle up.)
  104. MSG msg;
  105. while (PeekMessage(&msg, _hwndSite, WM_LBUTTONDOWN, WM_LBUTTONUP, PM_REMOVE))
  106. ; // Do nothing
  107. Pager_ForwardMouse(_hwndSite, FALSE);
  108. _hwndSite = NULL;
  109. _hwndParent = NULL;
  110. ATOMICRELEASE(_pmpParent);
  111. ATOMICRELEASE(_ptpbar);
  112. if (_hhookMsg)
  113. {
  114. TraceMsg(TF_MENUBAND, "CMenuAgent: Hook removed");
  115. UnhookWindowsHookEx(_hhookMsg);
  116. _hhookMsg = NULL;
  117. }
  118. _pvContext = NULL;
  119. }
  120. }
  121. /*----------------------------------------------------------
  122. Purpose: Make the menu go away
  123. */
  124. void CMenuAgent::CancelMenu(void* pvContext)
  125. {
  126. if (_pvContext == pvContext)
  127. {
  128. if (_hwndParent)
  129. {
  130. ASSERT(IS_VALID_HANDLE(_hwndParent, WND));
  131. TraceMsg(TF_MENUBAND, "Sending cancel mode to menu");
  132. // Use PostMessage so USER32 doesn't RIP on us in
  133. // MsgHook when it returns from the WM_MOUSEMOVE
  134. // that triggered this code path in the first place.
  135. PostMessage(_hwndParent, WM_CANCELMODE, 0, 0);
  136. // Disguise this as if the escape key was hit,
  137. // since this is called when the mouse hovers over
  138. // another menu sibling.
  139. _fEscHit = TRUE;
  140. // this can be called before a context is set up, so guard for NULL.
  141. if (_pmpParent)
  142. {
  143. _pmpParent->SetSubMenu(_ptpbar, FALSE);
  144. }
  145. }
  146. }
  147. }
  148. // store away the identity of the selected menu item.
  149. // if uFlags & MF_POPUP then i is the index.
  150. // otherwise it's the command and we need to convert it to the index.
  151. // we store index always because some popups don't have ids
  152. void CMenuAgent::_OnMenuSelect(HMENU hmenu, int i, UINT uFlags)
  153. {
  154. _uFlagsLastSelected = uFlags;
  155. _hmenuLastSelected = hmenu;
  156. }
  157. BOOL CMenuAgent::_OnKey(WPARAM vkey)
  158. {
  159. //
  160. // If the menu window is RTL mirrored, then the arrow keys should
  161. // be mirrored to reflect proper cursor movement. [samera]
  162. //
  163. if (IS_WINDOW_RTL_MIRRORED(_hwndSite))
  164. {
  165. switch (vkey)
  166. {
  167. case VK_LEFT:
  168. vkey = VK_RIGHT;
  169. break;
  170. case VK_RIGHT:
  171. vkey = VK_LEFT;
  172. break;
  173. }
  174. }
  175. switch (vkey)
  176. {
  177. case VK_RIGHT:
  178. if (!_hmenuLastSelected || !(_uFlagsLastSelected & MF_POPUP) || (_uFlagsLastSelected & MF_DISABLED) )
  179. {
  180. // if the currently selected item does not have a cascade, then
  181. // we need to cancel out of all of this and tell the top menu bar to go right
  182. _pmpParent->OnSelect(MPOS_SELECTRIGHT);
  183. }
  184. break;
  185. case VK_LEFT:
  186. if (!_hmenuLastSelected || _hmenuLastSelected == _ptpbar->GetPopupMenu()) {
  187. // if the currently selected menu item is in our top level menu,
  188. // then we need to cancel out of all this menu loop and tell the top menu bar
  189. // to go left
  190. _pmpParent->OnSelect(MPOS_SELECTLEFT);
  191. }
  192. break;
  193. default:
  194. return FALSE;
  195. }
  196. return TRUE;
  197. }
  198. /*----------------------------------------------------------
  199. Purpose: Message hook used to track keyboard and mouse messages
  200. while in a TrackPopupMenu modal loop.
  201. */
  202. LRESULT CMenuAgent::MsgHook(int nCode, WPARAM wParam, LPARAM lParam)
  203. {
  204. LRESULT lRet = 0;
  205. MSG * pmsg = (MSG *)lParam;
  206. switch (nCode)
  207. {
  208. case MSGF_MENU:
  209. switch (pmsg->message)
  210. {
  211. case WM_MENUSELECT:
  212. // keep track of the items as the are selected.
  213. g_menuagent._OnMenuSelect(GET_WM_MENUSELECT_HMENU(pmsg->wParam, pmsg->lParam),
  214. GET_WM_MENUSELECT_CMD(pmsg->wParam, pmsg->lParam),
  215. GET_WM_MENUSELECT_FLAGS(pmsg->wParam, pmsg->lParam));
  216. break;
  217. case WM_LBUTTONDOWN:
  218. case WM_RBUTTONDOWN:
  219. // Since we've received this msg, any previous escapes
  220. // (like escaping out of a cascaded menu) should be cleared
  221. // to prevent a false reason for termination.
  222. g_menuagent._fEscHit = FALSE;
  223. break;
  224. case WM_KEYDOWN:
  225. if (g_menuagent._OnKey(pmsg->wParam))
  226. break;
  227. case WM_SYSKEYDOWN:
  228. g_menuagent._fEscHit = (VK_ESCAPE == pmsg->wParam);
  229. break;
  230. case WM_MOUSEMOVE:
  231. // HACKHACK (isn't all of this a hack?): ignore zero-move
  232. // mouse moves, so the mouse does not contend with the keyboard.
  233. POINT pt;
  234. // In screen coords....
  235. pt.x = GET_X_LPARAM(pmsg->lParam);
  236. pt.y = GET_Y_LPARAM(pmsg->lParam);
  237. if (g_menuagent._ptLastMove.x == pt.x &&
  238. g_menuagent._ptLastMove.y == pt.y)
  239. {
  240. TraceMsg(TF_MENUBAND, "CMenuAgent: skipping dup mousemove");
  241. break;
  242. }
  243. g_menuagent._ptLastMove = pt;
  244. // Since we got a WM_MOUSEMOVE, we need to tell the Menuband global message hook.
  245. // We need to do this because this message hook steels all of the messages, and
  246. // the Menuband message hook never updates it's internal cache for removing duplicate
  247. // WM_MOUSEMOVE messages which cause problems as outlined in CMsgFilter::_HandleMouseMessages
  248. GetMessageFilter()->AcquireMouseLocation();
  249. // Forward the mouse moves to the toolbar so the toolbar still
  250. // has a chance to hot track. Must convert the points to the
  251. // toolbar's client space.
  252. ScreenToClient(g_menuagent._hwndSite, &pt);
  253. SendMessage(g_menuagent._hwndSite, pmsg->message, pmsg->wParam,
  254. MAKELPARAM(pt.x, pt.y));
  255. break;
  256. }
  257. break;
  258. default:
  259. if (0 > nCode)
  260. return CallNextHookEx(g_menuagent._hhookMsg, nCode, wParam, lParam);
  261. break;
  262. }
  263. // Pass it on to the next hook in the chain
  264. if (0 == lRet)
  265. lRet = CallNextHookEx(g_menuagent._hhookMsg, nCode, wParam, lParam);
  266. return lRet;
  267. }
  268. //=================================================================
  269. // Implementation of a menu deskbar object that uses TrackPopupMenu.
  270. //
  271. // This object uses traditional USER32 menus (via TrackPopupMenu)
  272. // to implement menu behavior. It uses the CMenuAgent object to
  273. // help get its work done. Since the menu deskbar site (_punkSite)
  274. // sits in a modal loop while any menu is up, it needs to know when
  275. // to quit its loop. The child object accomplishes this by sending
  276. // an OnSelect(MPOS_FULLCANCEL).
  277. //
  278. // The only time that TrackPopupMenu returns (but we don't want to
  279. // send an MPOS_FULLCANCEL) is if it's b/c the Escape key was hit.
  280. // This just means cancel the current level. Returning from Popup
  281. // is sufficient for this case. Otherwise, all other cases of
  282. // returning from TrackPopupMenu means we send a MPOS_FULLCANCEL.
  283. //
  284. // Summary:
  285. //
  286. // 1) User clicked outside the menu. This is a full cancel.
  287. // 2) User hit the Alt key. This is a full cancel.
  288. // 3) User hit the Esc key. This just cancels the current level.
  289. // (TrackPopupMenu handles this fine. No notification needs
  290. // to be sent b/c we want the top-level menu to stay in its
  291. // modal loop.)
  292. // 4) User selected a menu item. This is a full cancel.
  293. //
  294. //=================================================================
  295. #undef THISCLASS
  296. #undef SUPERCLASS
  297. #define SUPERCLASS CMenuDeskBar
  298. // Constructor
  299. CTrackPopupBar::CTrackPopupBar(void *pvContext, int id, HMENU hmenu, HWND hwnd)
  300. {
  301. _hmenu = hmenu;
  302. _hwndParent = hwnd;
  303. _id = id;
  304. _pvContext = pvContext;
  305. _nMBIgnoreNextDeselect = RegisterWindowMessage(TEXT("CMBIgnoreNextDeselect"));
  306. }
  307. // Destructor
  308. CTrackPopupBar::~CTrackPopupBar()
  309. {
  310. SetSite(NULL);
  311. }
  312. STDMETHODIMP_(ULONG) CTrackPopupBar::AddRef()
  313. {
  314. return SUPERCLASS::AddRef();
  315. }
  316. STDMETHODIMP_(ULONG) CTrackPopupBar::Release()
  317. {
  318. return SUPERCLASS::Release();
  319. }
  320. STDMETHODIMP CTrackPopupBar::QueryInterface(REFIID riid, void **ppvObj)
  321. {
  322. static const QITAB qit[] = {
  323. QITABENT(CTrackPopupBar, IMenuPopup),
  324. QITABENT(CTrackPopupBar, IObjectWithSite),
  325. { 0 },
  326. };
  327. HRESULT hres = QISearch(this, qit, riid, ppvObj);
  328. if (FAILED(hres))
  329. {
  330. hres = SUPERCLASS::QueryInterface(riid, ppvObj);
  331. }
  332. return hres;
  333. }
  334. /*----------------------------------------------------------
  335. Purpose: IServiceProvider::QueryService method
  336. */
  337. STDMETHODIMP CTrackPopupBar::QueryService(REFGUID guidService, REFIID riid, void **ppvObj)
  338. {
  339. if (IsEqualGUID(guidService, SID_SMenuBandChild))
  340. {
  341. if (IsEqualIID(riid, IID_IAccessible))
  342. {
  343. HRESULT hres = E_OUTOFMEMORY;
  344. CAccessible* pacc = new CAccessible(_hmenu, (WORD)_id);
  345. if (pacc)
  346. {
  347. hres = pacc->InitAcc();
  348. if (SUCCEEDED(hres))
  349. {
  350. hres = pacc->QueryInterface(riid, ppvObj);
  351. }
  352. pacc->Release();
  353. }
  354. return hres;
  355. }
  356. else
  357. return QueryInterface(riid, ppvObj);
  358. }
  359. else
  360. return SUPERCLASS::QueryService(guidService, riid, ppvObj);
  361. }
  362. /*----------------------------------------------------------
  363. Purpose: IMenuPopup::OnSelect method
  364. This allows the parent menubar to tell us when to
  365. bail out of the TrackPopupMenu
  366. */
  367. STDMETHODIMP CTrackPopupBar::OnSelect(DWORD dwType)
  368. {
  369. switch (dwType)
  370. {
  371. case MPOS_CANCELLEVEL:
  372. case MPOS_FULLCANCEL:
  373. g_menuagent.CancelMenu(_pvContext);
  374. break;
  375. default:
  376. TraceMsg(TF_WARNING, "CTrackPopupBar doesn't handle this MPOS_ value: %d", dwType);
  377. break;
  378. }
  379. return S_OK;
  380. }
  381. /*----------------------------------------------------------
  382. Purpose: IMenuPopup::SetSubMenu method
  383. */
  384. STDMETHODIMP CTrackPopupBar::SetSubMenu(IMenuPopup * pmp, BOOL bSet)
  385. {
  386. return E_NOTIMPL;
  387. }
  388. // HACKHACK: DO NOT TOUCH! This is the only way to select
  389. // the first item for a user menu. TrackMenuPopup by default does
  390. // not select the first item. We pump these messages to our window.
  391. // User snags these messages, and thinks the user pressed the down button
  392. // and selects the first item for us. The lParam is needed because Win95 gold
  393. // validated this message before using it. Another solution would be to listen
  394. // to WM_INITMENUPOPUP and look for the HWND of the menu. Then send that
  395. // window the private message MN_SELECTFIRSTVALIDITEM. But thats nasty compared
  396. // to this. - lamadio 1.5.99
  397. void CTrackPopupBar::SelectFirstItem()
  398. {
  399. HWND hwndFocus = GetFocus();
  400. // pulled the funny lparam numbers out of spy's butt.
  401. if (hwndFocus) {
  402. PostMessage(hwndFocus, WM_KEYDOWN, VK_DOWN, 0x11500001);
  403. PostMessage(hwndFocus, WM_KEYUP, VK_DOWN, 0xD1500001);
  404. #ifdef UNIX
  405. /* HACK HACK
  406. * The above PostMessages were causing the second menu item
  407. * to be selected if you access the menu from the keyboard.
  408. * The following PostMessages will nullify the above effect.
  409. * This is to make sure that menus in shdocvw work properly
  410. * with user32 menus.
  411. */
  412. PostMessage(hwndFocus, WM_KEYDOWN, VK_UP, 0x11500001);
  413. PostMessage(hwndFocus, WM_KEYUP, VK_UP, 0xD1500001);
  414. #endif /* UNIX */
  415. }
  416. }
  417. /*----------------------------------------------------------
  418. Purpose: IMenuPopup::Popup method
  419. Invoke the menu.
  420. */
  421. STDMETHODIMP CTrackPopupBar::Popup(POINTL *ppt, RECTL* prcExclude, DWORD dwFlags)
  422. {
  423. ASSERT(IS_VALID_READ_PTR(ppt, POINTL));
  424. ASSERT(NULL == prcExclude || IS_VALID_READ_PTR(prcExclude, RECTL));
  425. ASSERT(IS_VALID_CODE_PTR(_pmpParent, IMenuPopup));
  426. // We must be able to talk to the parent menu bar
  427. if (NULL == _pmpParent)
  428. return E_FAIL;
  429. ASSERT(IS_VALID_HANDLE(_hmenu, MENU));
  430. ASSERT(IS_VALID_CODE_PTR(_punkSite, IUnknown));
  431. HMENU hmenu = GetSubMenu(_hmenu, _id);
  432. HWND hwnd;
  433. TPMPARAMS tpm;
  434. TPMPARAMS * ptpm = NULL;
  435. // User32 does not want to fix this for compatibility reasons,
  436. // but TrackPopupMenu does not snap to the nearest monitor on Single and Multi-Mon
  437. // systems. This has the side effect that if we pass a non-visible coordinate, then
  438. // User places menu at a random location on screen. So instead, we're going to bias
  439. // the point to the monitor.
  440. MONITORINFO mi = {0};
  441. mi.cbSize = sizeof(mi);
  442. HMONITOR hMonitor = MonitorFromPoint(*((POINT*)ppt), MONITOR_DEFAULTTONEAREST);
  443. GetMonitorInfo(hMonitor, &mi);
  444. if (ppt->x >= mi.rcMonitor.right)
  445. ppt->x = mi.rcMonitor.right;
  446. if (ppt->y >= mi.rcMonitor.bottom)
  447. ppt->y = mi.rcMonitor.bottom;
  448. if (ppt->x <= mi.rcMonitor.left)
  449. ppt->x = mi.rcMonitor.left;
  450. if (ppt->y <= mi.rcMonitor.top)
  451. ppt->y = mi.rcMonitor.top;
  452. if (prcExclude)
  453. {
  454. tpm.cbSize = SIZEOF(tpm);
  455. tpm.rcExclude = *((LPRECT)prcExclude);
  456. ptpm = &tpm;
  457. }
  458. // The forwarding code in CShellBrowser::_ShouldForwardMenu
  459. // and CDocObjectHost::_ShouldForwardMenu expects the first
  460. // WM_MENUSELECT to be sent for the top-level menu item.
  461. //
  462. // We need to fake an initial menu select on the top menu band
  463. // to mimic USER and satisfy this expectation.
  464. //
  465. UINT uMSFlags = MF_POPUP;
  466. SendMessage(_hwndParent, WM_MENUSELECT, MAKEWPARAM(_id, uMSFlags), (LPARAM)_hmenu);
  467. SendMessage(_hwndParent, _nMBIgnoreNextDeselect, NULL, NULL);
  468. // Initialize the menu agent
  469. IUnknown_GetWindow(_punkSite, &hwnd);
  470. VARIANTARG v = {0};
  471. UINT uFlags = TPM_VERTICAL | TPM_TOPALIGN;
  472. UINT uAnimateFlags = 0;
  473. if (SUCCEEDED(IUnknown_Exec(_punkSite, &CGID_MENUDESKBAR, MBCID_GETSIDE, 0, NULL, &v))) {
  474. if (v.vt == VT_I4 &&
  475. (v.lVal == MENUBAR_RIGHT ||
  476. v.lVal == MENUBAR_LEFT))
  477. {
  478. uFlags = TPM_TOPALIGN;
  479. }
  480. switch (v.lVal)
  481. {
  482. case MENUBAR_LEFT: uAnimateFlags = TPM_HORNEGANIMATION;
  483. break;
  484. case MENUBAR_RIGHT: uAnimateFlags = TPM_HORPOSANIMATION;
  485. break;
  486. case MENUBAR_TOP: uAnimateFlags = TPM_VERNEGANIMATION;
  487. break;
  488. case MENUBAR_BOTTOM: uAnimateFlags = TPM_VERPOSANIMATION;
  489. break;
  490. }
  491. }
  492. g_menuagent.Init(_pvContext, this, _pmpParent, _hwndParent, hwnd);
  493. ASSERT(IS_VALID_HANDLE(hmenu, MENU));
  494. if (dwFlags & MPPF_INITIALSELECT)
  495. SelectFirstItem();
  496. uFlags |= uAnimateFlags;
  497. TrackPopupMenuEx(hmenu, uFlags,
  498. ppt->x, ppt->y, _hwndParent, ptpm);
  499. // Tell the parent that the menu is now gone
  500. SendMessage(_hwndParent, WM_MENUSELECT, MAKEWPARAM(0, 0xFFFF), NULL);
  501. g_menuagent.Reset(_pvContext);
  502. _pmpParent->SetSubMenu(this, FALSE);
  503. return S_FALSE;
  504. }