* Sync up to trunk HEAD (r62975).
[reactos.git] / base / shell / rshell / CMenuFocusManager.cpp
1 /*
2 * Shell Menu Band
3 *
4 * Copyright 2014 David Quintana
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19 */
20 #include "precomp.h"
21 #include <windowsx.h>
22 #include <commoncontrols.h>
23 #include <shlwapi_undoc.h>
24
25 #include "CMenuFocusManager.h"
26 #include "CMenuToolbars.h"
27 #include "CMenuBand.h"
28
29 #if DBG
30 # undef _ASSERT
31 # define _ASSERT(x) DbgAssert(!!(x), __FILE__, __LINE__, #x)
32
33 bool DbgAssert(bool x, const char * filename, int line, const char * expr)
34 {
35 if (!x)
36 {
37 char szMsg[512];
38 const char *fname;
39
40 fname = strrchr(filename, '\\');
41 if (fname == NULL)
42 {
43 fname = strrchr(filename, '/');
44 }
45
46 if (fname == NULL)
47 fname = filename;
48 else
49 fname++;
50
51 sprintf(szMsg, "%s:%d: Assertion failed: %s\n", fname, line, expr);
52
53 OutputDebugStringA(szMsg);
54
55 __debugbreak();
56 }
57 return x;
58 }
59 #else
60 # undef _ASSERT
61 # define _ASSERT(x) (!!(x))
62 #endif
63
64 WINE_DEFAULT_DEBUG_CHANNEL(CMenuFocus);
65
66 DWORD CMenuFocusManager::TlsIndex = 0;
67
68 CMenuFocusManager * CMenuFocusManager::GetManager()
69 {
70 return reinterpret_cast<CMenuFocusManager *>(TlsGetValue(TlsIndex));
71 }
72
73 CMenuFocusManager * CMenuFocusManager::AcquireManager()
74 {
75 CMenuFocusManager * obj = NULL;
76
77 if (!TlsIndex)
78 {
79 if ((TlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
80 return NULL;
81 }
82
83 obj = GetManager();
84
85 if (!obj)
86 {
87 obj = new CComObject<CMenuFocusManager>();
88 TlsSetValue(TlsIndex, obj);
89 }
90
91 obj->AddRef();
92
93 return obj;
94 }
95
96 void CMenuFocusManager::ReleaseManager(CMenuFocusManager * obj)
97 {
98 if (!obj->Release())
99 {
100 TlsSetValue(TlsIndex, NULL);
101 }
102 }
103
104 LRESULT CALLBACK CMenuFocusManager::s_MsgFilterHook(INT nCode, WPARAM wParam, LPARAM lParam)
105 {
106 return GetManager()->MsgFilterHook(nCode, wParam, lParam);
107 }
108
109 LRESULT CALLBACK CMenuFocusManager::s_GetMsgHook(INT nCode, WPARAM wParam, LPARAM lParam)
110 {
111 return GetManager()->GetMsgHook(nCode, wParam, lParam);
112 }
113
114 HRESULT CMenuFocusManager::PushToArray(StackEntryType type, CMenuBand * mb, HMENU hmenu)
115 {
116 if (m_bandCount >= MAX_RECURSE)
117 return E_OUTOFMEMORY;
118
119 m_bandStack[m_bandCount].type = type;
120 m_bandStack[m_bandCount].mb = mb;
121 m_bandStack[m_bandCount].hmenu = hmenu;
122 m_bandCount++;
123
124 return S_OK;
125 }
126
127 HRESULT CMenuFocusManager::PopFromArray(StackEntryType * pType, CMenuBand ** pMb, HMENU * pHmenu)
128 {
129 if (pType) *pType = NoEntry;
130 if (pMb) *pMb = NULL;
131 if (pHmenu) *pHmenu = NULL;
132
133 if (m_bandCount <= 0)
134 return S_FALSE;
135
136 m_bandCount--;
137
138 if (pType) *pType = m_bandStack[m_bandCount].type;
139 if (*pType == TrackedMenuEntry)
140 {
141 if (pHmenu) *pHmenu = m_bandStack[m_bandCount].hmenu;
142 }
143 else
144 {
145 if (pMb) *pMb = m_bandStack[m_bandCount].mb;
146 }
147
148 return S_OK;
149 }
150
151 CMenuFocusManager::CMenuFocusManager() :
152 m_current(NULL),
153 m_parent(NULL),
154 m_hMsgFilterHook(NULL),
155 m_hGetMsgHook(NULL),
156 m_mouseTrackDisabled(FALSE),
157 m_captureHwnd(0),
158 m_hwndUnderMouse(NULL),
159 m_entryUnderMouse(NULL),
160 m_selectedMenu(NULL),
161 m_selectedItem(0),
162 m_selectedItemFlags(0),
163 m_bandCount(0)
164 {
165 m_ptPrev.x = 0;
166 m_ptPrev.y = 0;
167 m_threadId = GetCurrentThreadId();
168 }
169
170 CMenuFocusManager::~CMenuFocusManager()
171 {
172 }
173
174 void CMenuFocusManager::DisableMouseTrack(HWND parent, BOOL disableThis)
175 {
176 BOOL bDisable = FALSE;
177 BOOL lastDisable = FALSE;
178
179 int i = m_bandCount;
180 while (--i >= 0)
181 {
182 StackEntry& entry = m_bandStack[i];
183
184 if (entry.type != TrackedMenuEntry)
185 {
186 HWND hwnd;
187 HRESULT hr = entry.mb->_GetTopLevelWindow(&hwnd);
188 if (FAILED_UNEXPECTEDLY(hr))
189 break;
190
191 if (hwnd == parent)
192 {
193 lastDisable = disableThis;
194 entry.mb->_DisableMouseTrack(disableThis);
195 bDisable = TRUE;
196 }
197 else
198 {
199 lastDisable = bDisable;
200 entry.mb->_DisableMouseTrack(bDisable);
201 }
202 }
203 }
204 m_mouseTrackDisabled = lastDisable;
205 }
206
207 void CMenuFocusManager::SetCapture(HWND child)
208 {
209 if (m_captureHwnd != child)
210 {
211 if (child)
212 {
213 ::SetCapture(child);
214 m_captureHwnd = child;
215 TRACE("MouseTrack is now capturing %p\n", child);
216 }
217 else
218 {
219 ::ReleaseCapture();
220 m_captureHwnd = NULL;
221 TRACE("MouseTrack is now off\n");
222 }
223
224 }
225 }
226
227 HRESULT CMenuFocusManager::IsTrackedWindow(HWND hWnd, StackEntry ** pentry)
228 {
229 if (pentry)
230 *pentry = NULL;
231
232 for (int i = m_bandCount; --i >= 0;)
233 {
234 StackEntry& entry = m_bandStack[i];
235
236 if (entry.type != TrackedMenuEntry)
237 {
238 HRESULT hr = entry.mb->IsWindowOwner(hWnd);
239 if (FAILED_UNEXPECTEDLY(hr))
240 return hr;
241 if (hr == S_OK)
242 {
243 if (pentry)
244 *pentry = &entry;
245 return S_OK;
246 }
247 }
248 }
249
250 return S_FALSE;
251 }
252
253 LRESULT CMenuFocusManager::ProcessMouseMove(MSG* msg)
254 {
255 HWND child;
256 int iHitTestResult = -1;
257
258 POINT pt2 = { GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam) };
259 ClientToScreen(msg->hwnd, &pt2);
260
261 // Don't do anything if the mouse has not been moved
262 POINT pt = msg->pt;
263 if (pt.x == m_ptPrev.x && pt.y == m_ptPrev.y)
264 return TRUE;
265
266 // Don't do anything if another window is capturing the mouse.
267 HWND cCapture = ::GetCapture();
268 if (cCapture && cCapture != m_captureHwnd && m_current->type != TrackedMenuEntry)
269 return TRUE;
270
271 m_ptPrev = pt;
272
273 child = WindowFromPoint(pt);
274
275 StackEntry * entry = NULL;
276 IsTrackedWindow(child, &entry);
277
278 BOOL isTracking = FALSE;
279 if (entry)
280 {
281 ScreenToClient(child, &pt);
282 iHitTestResult = SendMessageW(child, TB_HITTEST, 0, (LPARAM) &pt);
283 isTracking = entry->mb->_IsTracking();
284
285 if (SendMessage(child, WM_USER_ISTRACKEDITEM, iHitTestResult, 0) == S_FALSE)
286 {
287 TRACE("Hot item tracking detected a change (capture=%p)...\n", m_captureHwnd);
288 DisableMouseTrack(NULL, FALSE);
289 if (isTracking && iHitTestResult>=0 && m_current->type == TrackedMenuEntry)
290 SendMessage(entry->hwnd, WM_CANCELMODE, 0, 0);
291 PostMessage(child, WM_USER_CHANGETRACKEDITEM, iHitTestResult, MAKELPARAM(isTracking, TRUE));
292 if (m_current->type == TrackedMenuEntry)
293 return FALSE;
294 }
295 }
296
297 if (m_entryUnderMouse != entry)
298 {
299 // Mouse moved away from a tracked window
300 if (m_entryUnderMouse)
301 {
302 m_entryUnderMouse->mb->_ChangeHotItem(NULL, -1, HICF_MOUSE);
303 }
304 if (cCapture == m_captureHwnd)
305 SetCapture(NULL);
306 }
307
308 if (m_hwndUnderMouse != child)
309 {
310 if (entry)
311 {
312 // Mouse moved to a tracked window
313 if (m_current->type == MenuPopupEntry)
314 {
315 ScreenToClient(child, &pt2);
316 SendMessage(child, WM_MOUSEMOVE, msg->wParam, MAKELPARAM(pt2.x, pt2.y));
317 }
318 }
319
320 m_hwndUnderMouse = child;
321 m_entryUnderMouse = entry;
322 }
323
324 if (m_current->type == MenuPopupEntry)
325 {
326 HWND parent = GetAncestor(child, GA_ROOT);
327 DisableMouseTrack(parent, FALSE);
328 }
329
330 return TRUE;
331 }
332
333 LRESULT CMenuFocusManager::MsgFilterHook(INT nCode, WPARAM hookWParam, LPARAM hookLParam)
334 {
335 if (nCode < 0)
336 return CallNextHookEx(m_hMsgFilterHook, nCode, hookWParam, hookLParam);
337
338 if (nCode == MSGF_MENU)
339 {
340 BOOL callNext = TRUE;
341 MSG* msg = reinterpret_cast<MSG*>(hookLParam);
342
343 switch (msg->message)
344 {
345 case WM_NCLBUTTONDOWN:
346 case WM_LBUTTONDOWN:
347 if (m_menuBar)
348 {
349 POINT pt = msg->pt;
350 HWND child = WindowFromPoint(pt);
351 BOOL hoveringMenuBar = m_menuBar->mb->IsWindowOwner(child) == S_OK;
352 if (hoveringMenuBar)
353 {
354 m_menuBar->mb->_DisableMouseTrack(TRUE);
355 }
356 }
357 break;
358 case WM_MOUSEMOVE:
359 callNext = ProcessMouseMove(msg);
360 break;
361 case WM_INITMENUPOPUP:
362 TRACE("WM_INITMENUPOPUP %p %p\n", msg->wParam, msg->lParam);
363 m_selectedMenu = reinterpret_cast<HMENU>(msg->lParam);
364 m_selectedItem = -1;
365 m_selectedItemFlags = 0;
366 break;
367 case WM_MENUSELECT:
368 TRACE("WM_MENUSELECT %p %p\n", msg->wParam, msg->lParam);
369 m_selectedMenu = reinterpret_cast<HMENU>(msg->lParam);
370 m_selectedItem = GET_X_LPARAM(msg->wParam);
371 m_selectedItemFlags = HIWORD(msg->wParam);
372 break;
373 case WM_KEYDOWN:
374 switch (msg->wParam)
375 {
376 case VK_LEFT:
377 if (m_current->hmenu == m_selectedMenu)
378 {
379 m_parent->mb->_MenuItemHotTrack(VK_LEFT);
380 }
381 break;
382 case VK_RIGHT:
383 if (m_selectedItem < 0 || !(m_selectedItemFlags & MF_POPUP))
384 {
385 m_parent->mb->_MenuItemHotTrack(VK_RIGHT);
386 }
387 break;
388 }
389 break;
390 }
391
392 if (!callNext)
393 return 1;
394 }
395
396 return CallNextHookEx(m_hMsgFilterHook, nCode, hookWParam, hookLParam);
397 }
398
399 LRESULT CMenuFocusManager::GetMsgHook(INT nCode, WPARAM hookWParam, LPARAM hookLParam)
400 {
401 if (nCode < 0)
402 return CallNextHookEx(m_hGetMsgHook, nCode, hookWParam, hookLParam);
403
404 if (nCode == HC_ACTION)
405 {
406 BOOL callNext = TRUE;
407 MSG* msg = reinterpret_cast<MSG*>(hookLParam);
408 POINT pt = msg->pt;
409
410 switch (msg->message)
411 {
412 case WM_NCLBUTTONDOWN:
413 case WM_LBUTTONDOWN:
414 if (m_current->type == MenuPopupEntry)
415 {
416 HWND child = WindowFromPoint(pt);
417
418 if (IsTrackedWindow(child) != S_OK)
419 {
420 SetCapture(NULL);
421 m_current->mb->_MenuItemHotTrack(MPOS_FULLCANCEL);
422 }
423 }
424 break;
425 case WM_MOUSEMOVE:
426 callNext = ProcessMouseMove(msg);
427 break;
428 case WM_MOUSELEAVE:
429 callNext = ProcessMouseMove(msg);
430 //callNext = ProcessMouseLeave(msg);
431 break;
432 case WM_SYSKEYDOWN:
433 case WM_KEYDOWN:
434 if (m_current->type == MenuPopupEntry)
435 {
436 DisableMouseTrack(m_current->hwnd, TRUE);
437 switch (msg->wParam)
438 {
439 case VK_ESCAPE:
440 case VK_MENU:
441 case VK_LMENU:
442 case VK_RMENU:
443 m_current->mb->_MenuItemHotTrack(MPOS_FULLCANCEL);
444 break;
445 case VK_RETURN:
446 m_current->mb->_MenuItemHotTrack(MPOS_EXECUTE);
447 break;
448 case VK_LEFT:
449 m_current->mb->_MenuItemHotTrack(VK_LEFT);
450 break;
451 case VK_RIGHT:
452 m_current->mb->_MenuItemHotTrack(VK_RIGHT);
453 break;
454 case VK_UP:
455 m_current->mb->_MenuItemHotTrack(VK_UP);
456 break;
457 case VK_DOWN:
458 m_current->mb->_MenuItemHotTrack(VK_DOWN);
459 break;
460 }
461 msg->message = WM_NULL;
462 msg->lParam = 0;
463 msg->wParam = 0;
464 }
465 break;
466 }
467
468 if (!callNext)
469 return 1;
470 }
471
472 return CallNextHookEx(m_hGetMsgHook, nCode, hookWParam, hookLParam);
473 }
474
475 HRESULT CMenuFocusManager::PlaceHooks()
476 {
477 if (m_current->type == TrackedMenuEntry)
478 {
479 TRACE("Entering MSGFILTER hook...\n");
480 m_hMsgFilterHook = SetWindowsHookEx(WH_MSGFILTER, s_MsgFilterHook, NULL, m_threadId);
481 }
482 else
483 {
484 TRACE("Entering GETMESSAGE hook...\n");
485 m_hGetMsgHook = SetWindowsHookEx(WH_GETMESSAGE, s_GetMsgHook, NULL, m_threadId);
486 }
487 return S_OK;
488 }
489
490 HRESULT CMenuFocusManager::RemoveHooks()
491 {
492 TRACE("Removing all hooks...\n");
493 if (m_hMsgFilterHook)
494 UnhookWindowsHookEx(m_hMsgFilterHook);
495 if (m_hGetMsgHook)
496 UnhookWindowsHookEx(m_hGetMsgHook);
497 m_hMsgFilterHook = NULL;
498 m_hGetMsgHook = NULL;
499 return S_OK;
500 }
501
502 HRESULT CMenuFocusManager::UpdateFocus()
503 {
504 HRESULT hr;
505 StackEntry * old = m_current;
506
507 if (old)
508 SetCapture(NULL);
509
510 if (m_bandCount > 0)
511 m_current = &(m_bandStack[m_bandCount - 1]);
512 else
513 m_current = NULL;
514
515 if (m_current && m_current->type != TrackedMenuEntry)
516 {
517 hr = m_current->mb->_GetTopLevelWindow(&(m_current->hwnd));
518 if (FAILED_UNEXPECTEDLY(hr))
519 return hr;
520 }
521
522 if (m_bandCount >= 2)
523 {
524 m_parent = &(m_bandStack[m_bandCount - 2]);
525 _ASSERT(m_parent->type != TrackedMenuEntry);
526 }
527 else
528 {
529 m_parent = NULL;
530 }
531
532 if (m_bandCount >= 1 && m_bandStack[0].type == MenuBarEntry)
533 {
534 m_menuBar = &(m_bandStack[0]);
535 }
536 else
537 {
538 m_menuBar = NULL;
539 }
540
541 if (old && (!m_current || old->type != m_current->type))
542 {
543 if (m_current && m_current->type != TrackedMenuEntry)
544 {
545 DisableMouseTrack(m_current->hwnd, FALSE);
546 }
547
548 hr = RemoveHooks();
549 if (FAILED_UNEXPECTEDLY(hr))
550 return hr;
551 }
552
553 if (m_current && (!old || old->type != m_current->type))
554 {
555 hr = PlaceHooks();
556 if (FAILED_UNEXPECTEDLY(hr))
557 return hr;
558 }
559
560 if (m_parent)
561 {
562 DisableMouseTrack(m_parent->hwnd, TRUE);
563 }
564
565 if ((m_current && m_current->type == MenuPopupEntry) &&
566 (!m_parent || m_parent->type == MenuBarEntry))
567 {
568 // When the mouse moves, it should set itself to the proper band
569 SetCapture(m_current->hwnd);
570
571 if (old && old->type == TrackedMenuEntry)
572 {
573 // FIXME: Debugging code, probably not right
574 POINT pt2;
575 RECT rc2;
576 GetCursorPos(&pt2);
577 ScreenToClient(m_current->hwnd, &pt2);
578 GetClientRect(m_current->hwnd, &rc2);
579 if (PtInRect(&rc2, pt2))
580 SendMessage(m_current->hwnd, WM_MOUSEMOVE, 0, MAKELPARAM(pt2.x, pt2.y));
581 else
582 SendMessage(m_current->hwnd, WM_MOUSELEAVE, 0, 0);
583 }
584 }
585
586 _ASSERT(!m_parent || m_parent->type != TrackedMenuEntry);
587
588 return S_OK;
589 }
590
591 HRESULT CMenuFocusManager::PushMenuBar(CMenuBand * mb)
592 {
593 _ASSERT(m_bandCount == 0);
594
595 HRESULT hr = PushToArray(MenuBarEntry, mb, NULL);
596 if (FAILED_UNEXPECTEDLY(hr))
597 return hr;
598
599 return UpdateFocus();
600 }
601
602 HRESULT CMenuFocusManager::PushMenuPopup(CMenuBand * mb)
603 {
604 _ASSERT(!m_current || m_current->type != TrackedMenuEntry);
605
606 HRESULT hr = PushToArray(MenuPopupEntry, mb, NULL);
607 if (FAILED_UNEXPECTEDLY(hr))
608 return hr;
609
610 hr = UpdateFocus();
611
612 if (m_parent && m_parent->type != TrackedMenuEntry)
613 {
614 m_parent->mb->_SetChildBand(mb);
615 mb->_SetParentBand(m_parent->mb);
616 }
617
618 return hr;
619 }
620
621 HRESULT CMenuFocusManager::PushTrackedPopup(HMENU popup)
622 {
623 _ASSERT(m_bandCount > 0);
624 _ASSERT(!m_current || m_current->type != TrackedMenuEntry);
625
626 HRESULT hr = PushToArray(TrackedMenuEntry, NULL, popup);
627 if (FAILED_UNEXPECTEDLY(hr))
628 return hr;
629
630 TRACE("PushTrackedPopup %p\n", popup);
631 m_selectedMenu = popup;
632 m_selectedItem = -1;
633 m_selectedItemFlags = 0;
634
635 return UpdateFocus();
636 }
637
638 HRESULT CMenuFocusManager::PopMenuBar(CMenuBand * mb)
639 {
640 StackEntryType type;
641 CMenuBand * mbc;
642 HRESULT hr;
643
644 hr = PopFromArray(&type, &mbc, NULL);
645 if (FAILED_UNEXPECTEDLY(hr))
646 {
647 UpdateFocus();
648 return hr;
649 }
650
651 _ASSERT(type == MenuBarEntry);
652 if (type != MenuBarEntry)
653 return E_FAIL;
654
655 if (!mbc)
656 return E_FAIL;
657
658 mbc->_SetParentBand(NULL);
659
660 hr = UpdateFocus();
661 if (FAILED_UNEXPECTEDLY(hr))
662 return hr;
663
664 if (m_current)
665 {
666 _ASSERT(m_current->type != TrackedMenuEntry);
667 m_current->mb->_SetChildBand(NULL);
668 }
669
670 return S_OK;
671 }
672
673 HRESULT CMenuFocusManager::PopMenuPopup(CMenuBand * mb)
674 {
675 StackEntryType type;
676 CMenuBand * mbc;
677 HRESULT hr;
678
679 hr = PopFromArray(&type, &mbc, NULL);
680 if (FAILED_UNEXPECTEDLY(hr))
681 {
682 UpdateFocus();
683 return hr;
684 }
685
686 _ASSERT(type == MenuPopupEntry);
687 if (type != MenuPopupEntry)
688 return E_FAIL;
689
690 if (!mbc)
691 return E_FAIL;
692
693 mbc->_SetParentBand(NULL);
694
695 hr = UpdateFocus();
696 if (FAILED_UNEXPECTEDLY(hr))
697 return hr;
698
699 if (m_current)
700 {
701 _ASSERT(m_current->type != TrackedMenuEntry);
702 m_current->mb->_SetChildBand(NULL);
703 }
704
705 return S_OK;
706 }
707
708 HRESULT CMenuFocusManager::PopTrackedPopup(HMENU popup)
709 {
710 StackEntryType type;
711 HMENU hmenu;
712 HRESULT hr;
713
714 hr = PopFromArray(&type, NULL, &hmenu);
715 if (FAILED_UNEXPECTEDLY(hr))
716 {
717 UpdateFocus();
718 return hr;
719 }
720
721 _ASSERT(type == TrackedMenuEntry);
722 if (type != TrackedMenuEntry)
723 return E_FAIL;
724
725 if (hmenu != popup)
726 return E_FAIL;
727
728 hr = UpdateFocus();
729 if (FAILED_UNEXPECTEDLY(hr))
730 return hr;
731
732 return S_OK;
733 }