[USER32] Fix Task Switcher (#976)
[reactos.git] / win32ss / user / user32 / controls / appswitch.c
1 /*
2 * COPYRIGHT: See COPYING in the top level directory
3 * PROJECT: ReactOS system libraries
4 * FILE: win32ss/user/user32/controls/appswitch.c
5 * PURPOSE: app switching functionality
6 * PROGRAMMERS: Johannes Anderwald (johannes.anderwald@reactos.org)
7 * David Quintana (gigaherz@gmail.com)
8 * Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
9 */
10
11 //
12 // TODO:
13 // Move to Win32k.
14 // Add registry support.
15 //
16 //
17
18 #include <user32.h>
19
20 WINE_DEFAULT_DEBUG_CHANNEL(user32);
21
22 #define DIALOG_MARGIN 8 // margin of dialog contents
23
24 #define CX_ICON 32 // width of icon
25 #define CY_ICON 32 // height of icon
26 #define ICON_MARGIN 4 // margin width around an icon
27
28 #define CX_ITEM (CX_ICON + 2 * ICON_MARGIN)
29 #define CY_ITEM (CY_ICON + 2 * ICON_MARGIN)
30 #define ITEM_MARGIN 4 // margin width around an item
31
32 #define CX_ITEM_SPACE (CX_ITEM + 2 * ITEM_MARGIN)
33 #define CY_ITEM_SPACE (CY_ITEM + 2 * ITEM_MARGIN)
34
35 #define CY_TEXT_MARGIN 4 // margin height around text
36
37 // limit the number of windows shown in the alt-tab window
38 // 120 windows results in (12*40) by (10*40) pixels worth of icons.
39 #define MAX_WINDOWS 120
40
41 // Global variables
42 HWND switchdialog = NULL;
43 HFONT dialogFont;
44 int selectedWindow = 0;
45 BOOL isOpen = FALSE;
46
47 int fontHeight=0;
48
49 WCHAR windowText[1024];
50
51 HWND windowList[MAX_WINDOWS];
52 HICON iconList[MAX_WINDOWS];
53 int windowCount = 0;
54
55 int cxBorder, cyBorder;
56 int nItems, nCols, nRows;
57 int itemsW, itemsH;
58 int totalW, totalH;
59 int xOffset, yOffset;
60 POINT ptStart;
61
62 int nShift = 0;
63
64 BOOL Esc = FALSE;
65
66 BOOL CoolSwitch = TRUE;
67 int CoolSwitchRows = 3;
68 int CoolSwitchColumns = 7;
69
70 // window style
71 const DWORD Style = WS_POPUP | WS_BORDER | WS_DISABLED;
72 const DWORD ExStyle = WS_EX_TOPMOST | WS_EX_DLGMODALFRAME | WS_EX_TOOLWINDOW;
73
74 DWORD wtodw(const WCHAR *psz)
75 {
76 const WCHAR *pch = psz;
77 DWORD Value = 0;
78 while ('0' <= *pch && *pch <= '9')
79 {
80 Value *= 10;
81 Value += *pch - L'0';
82 }
83 return Value;
84 }
85
86 BOOL LoadCoolSwitchSettings(void)
87 {
88 CoolSwitch = TRUE;
89 CoolSwitchRows = 3;
90 CoolSwitchColumns = 7;
91
92 // FIXME: load the settings from registry
93
94 TRACE("CoolSwitch: %d\n", CoolSwitch);
95 TRACE("CoolSwitchRows: %d\n", CoolSwitchRows);
96 TRACE("CoolSwitchColumns: %d\n", CoolSwitchColumns);
97
98 return TRUE;
99 }
100
101 void ResizeAndCenter(HWND hwnd, int width, int height)
102 {
103 int x, y;
104 RECT Rect;
105
106 int screenwidth = GetSystemMetrics(SM_CXSCREEN);
107 int screenheight = GetSystemMetrics(SM_CYSCREEN);
108
109 x = (screenwidth - width) / 2;
110 y = (screenheight - height) / 2;
111
112 SetRect(&Rect, x, y, x + width, y + height);
113 AdjustWindowRectEx(&Rect, Style, FALSE, ExStyle);
114
115 x = Rect.left;
116 y = Rect.top;
117 width = Rect.right - Rect.left;
118 height = Rect.bottom - Rect.top;
119 MoveWindow(hwnd, x, y, width, height, FALSE);
120
121 ptStart.x = x;
122 ptStart.y = y;
123 }
124
125 void MakeWindowActive(HWND hwnd)
126 {
127 if (IsIconic(hwnd))
128 PostMessageW(hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);
129
130 BringWindowToTop(hwnd); // same as: SetWindowPos(hwnd,HWND_TOP,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE); ?
131 SetForegroundWindow(hwnd);
132 }
133
134 void CompleteSwitch(BOOL doSwitch)
135 {
136 if (!isOpen)
137 return;
138
139 isOpen = FALSE;
140
141 TRACE("[ATbot] CompleteSwitch Hiding Window.\n");
142 ShowWindow(switchdialog, SW_HIDE);
143
144 if(doSwitch)
145 {
146 if(selectedWindow >= windowCount)
147 return;
148
149 // FIXME: workaround because reactos fails to activate the previous window correctly.
150 //if(selectedWindow != 0)
151 {
152 HWND hwnd = windowList[selectedWindow];
153
154 GetWindowTextW(hwnd, windowText, _countof(windowText));
155
156 TRACE("[ATbot] CompleteSwitch Switching to 0x%08x (%ls)\n", hwnd, windowText);
157
158 MakeWindowActive(hwnd);
159 }
160 }
161
162 windowCount = 0;
163 }
164
165 BOOL CALLBACK EnumerateCallback(HWND window, LPARAM lParam)
166 {
167 HICON hIcon;
168
169 UNREFERENCED_PARAMETER(lParam);
170
171 if (!IsWindowVisible(window))
172 return TRUE;
173
174 if (GetWindow(window, GW_OWNER) != NULL)
175 return TRUE;
176
177 GetClassNameW(window, windowText, _countof(windowText));
178 if ((wcscmp(L"Shell_TrayWnd", windowText)==0) ||
179 (wcscmp(L"Progman", windowText)==0) )
180 return TRUE;
181
182 // First try to get the big icon assigned to the window
183 hIcon = (HICON)SendMessageW(window, WM_GETICON, ICON_BIG, 0);
184 if (!hIcon)
185 {
186 // If no icon is assigned, try to get the icon assigned to the windows' class
187 hIcon = (HICON)GetClassLongPtrW(window, GCL_HICON);
188 if (!hIcon)
189 {
190 // If we still don't have an icon, see if we can do with the small icon,
191 // or a default application icon
192 hIcon = (HICON)SendMessageW(window, WM_GETICON, ICON_SMALL2, 0);
193 if (!hIcon)
194 {
195 // using windows logo icon as default
196 hIcon = gpsi->hIconWindows;
197 if (!hIcon)
198 {
199 //if all attempts to get icon fails go to the next window
200 return TRUE;
201 }
202 }
203 }
204 }
205
206 windowList[windowCount] = window;
207 iconList[windowCount] = CopyIcon(hIcon);
208
209 windowCount++;
210
211 // If we got to the max number of windows,
212 // we won't be able to add any more
213 if(windowCount >= MAX_WINDOWS)
214 return FALSE;
215
216 return TRUE;
217 }
218
219 // Function mostly compatible with the normal EnumWindows,
220 // except it lists in Z-Order and it doesn't ensure consistency
221 // if a window is removed while enumerating
222 void EnumWindowsZOrder(WNDENUMPROC callback, LPARAM lParam)
223 {
224 HWND next = GetTopWindow(NULL);
225 while (next != NULL)
226 {
227 if(!callback(next, lParam))
228 break;
229 next = GetWindow(next, GW_HWNDNEXT);
230 }
231 }
232
233 void ProcessMouseMessage(UINT message, LPARAM lParam)
234 {
235 int xPos = LOWORD(lParam);
236 int yPos = HIWORD(lParam);
237
238 int xIndex = (xPos - DIALOG_MARGIN) / CX_ITEM_SPACE;
239 int yIndex = (yPos - DIALOG_MARGIN) / CY_ITEM_SPACE;
240
241 if (xIndex < 0 || nCols <= xIndex ||
242 yIndex < 0 || nRows <= yIndex)
243 {
244 return;
245 }
246
247 selectedWindow = (yIndex*nCols) + xIndex;
248 if (message == WM_MOUSEMOVE)
249 {
250 InvalidateRect(switchdialog, NULL, TRUE);
251 //RedrawWindow(switchdialog, NULL, NULL, 0);
252 }
253 else
254 {
255 selectedWindow = (yIndex*nCols) + xIndex;
256 CompleteSwitch(TRUE);
257 }
258 }
259
260 void OnPaint(HWND hWnd)
261 {
262 HDC dialogDC;
263 PAINTSTRUCT paint;
264 RECT cRC, textRC;
265 int i, xPos, yPos, CharCount;
266 HFONT dcFont;
267 HICON hIcon;
268 HPEN hPen;
269 COLORREF Color;
270
271 // check
272 if (nCols == 0 || nItems == 0)
273 return;
274
275 // begin painting
276 dialogDC = BeginPaint(hWnd, &paint);
277 if (dialogDC == NULL)
278 return;
279
280 // fill the client area
281 GetClientRect(hWnd, &cRC);
282 FillRect(dialogDC, &cRC, (HBRUSH)(COLOR_3DFACE + 1));
283
284 // if the selection index exceeded the display items, then
285 // do display item shifting
286 if (selectedWindow >= nItems)
287 nShift = selectedWindow - nItems + 1;
288 else
289 nShift = 0;
290
291 for (i = 0; i < nItems; ++i)
292 {
293 // get the icon to display
294 hIcon = iconList[i + nShift];
295
296 // calculate the position where we start drawing
297 xPos = DIALOG_MARGIN + CX_ITEM_SPACE * (i % nCols) + ITEM_MARGIN;
298 yPos = DIALOG_MARGIN + CY_ITEM_SPACE * (i / nCols) + ITEM_MARGIN;
299
300 // centering
301 if (nItems < CoolSwitchColumns)
302 {
303 xPos += (itemsW - nItems * CX_ITEM_SPACE) / 2;
304 }
305
306 // if this position is selected,
307 if (selectedWindow == i + nShift)
308 {
309 // create a solid pen
310 Color = GetSysColor(COLOR_HIGHLIGHT);
311 hPen = CreatePen(PS_SOLID, 1, Color);
312
313 // draw a rectangle with using the pen
314 SelectObject(dialogDC, hPen);
315 SelectObject(dialogDC, GetStockObject(NULL_BRUSH));
316 Rectangle(dialogDC, xPos, yPos, xPos + CX_ITEM, yPos + CY_ITEM);
317 Rectangle(dialogDC, xPos + 1, yPos + 1,
318 xPos + CX_ITEM - 1, yPos + CY_ITEM - 1);
319
320 // delete the pen
321 DeleteObject(hPen);
322 }
323
324 // draw icon
325 DrawIconEx(dialogDC, xPos + ICON_MARGIN, yPos + ICON_MARGIN,
326 hIcon, CX_ICON, CY_ICON, 0, NULL, DI_NORMAL);
327 }
328
329 // set the text rectangle
330 SetRect(&textRC, DIALOG_MARGIN, DIALOG_MARGIN + itemsH,
331 totalW - DIALOG_MARGIN, totalH - DIALOG_MARGIN);
332
333 // draw the sunken button around text
334 DrawFrameControl(dialogDC, &textRC, DFC_BUTTON,
335 DFCS_BUTTONPUSH | DFCS_PUSHED);
336
337 // get text
338 CharCount = GetWindowTextW(windowList[selectedWindow], windowText,
339 _countof(windowText));
340
341 // draw text
342 dcFont = SelectObject(dialogDC, dialogFont);
343 SetTextColor(dialogDC, GetSysColor(COLOR_BTNTEXT));
344 SetBkMode(dialogDC, TRANSPARENT);
345 DrawTextW(dialogDC, windowText, CharCount, &textRC,
346 DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS | DT_SINGLELINE);
347 SelectObject(dialogDC, dcFont);
348
349 // end painting
350 EndPaint(hWnd, &paint);
351 }
352
353 DWORD CreateSwitcherWindow(HINSTANCE hInstance)
354 {
355 switchdialog = CreateWindowExW( WS_EX_TOPMOST|WS_EX_DLGMODALFRAME|WS_EX_TOOLWINDOW,
356 WC_SWITCH,
357 L"",
358 WS_POPUP|WS_BORDER|WS_DISABLED,
359 CW_USEDEFAULT,
360 CW_USEDEFAULT,
361 400, 150,
362 NULL, NULL,
363 hInstance, NULL);
364 if (!switchdialog)
365 {
366 TRACE("[ATbot] Task Switcher Window failed to create.\n");
367 return 0;
368 }
369
370 isOpen = FALSE;
371 return 1;
372 }
373
374 DWORD GetDialogFont(VOID)
375 {
376 HDC tDC;
377 TEXTMETRIC tm;
378
379 dialogFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
380
381 tDC = GetDC(0);
382 GetTextMetrics(tDC, &tm);
383 fontHeight = tm.tmHeight;
384 ReleaseDC(0, tDC);
385
386 return 1;
387 }
388
389 void PrepareWindow(VOID)
390 {
391 nItems = windowCount;
392
393 nCols = CoolSwitchColumns;
394 nRows = (nItems + CoolSwitchColumns - 1) / CoolSwitchColumns;
395 if (nRows > CoolSwitchRows)
396 {
397 nRows = CoolSwitchRows;
398 nItems = nRows * nCols;
399 }
400
401 itemsW = nCols * CX_ITEM_SPACE;
402 itemsH = nRows * CY_ITEM_SPACE;
403
404 totalW = itemsW + 2 * DIALOG_MARGIN;
405 totalH = itemsH + 2 * DIALOG_MARGIN;
406 totalH += fontHeight + 2 * CY_TEXT_MARGIN;
407
408 ResizeAndCenter(switchdialog, totalW, totalH);
409 }
410
411 BOOL ProcessHotKey(VOID)
412 {
413 if (!isOpen)
414 {
415 windowCount=0;
416 EnumWindowsZOrder(EnumerateCallback, 0);
417
418 if (windowCount < 2)
419 return FALSE;
420
421 selectedWindow = 1;
422
423 TRACE("[ATbot] HotKey Received. Opening window.\n");
424 ShowWindow(switchdialog, SW_SHOWNORMAL);
425 MakeWindowActive(switchdialog);
426 isOpen = TRUE;
427 }
428 else
429 {
430 TRACE("[ATbot] HotKey Received Rotating.\n");
431 selectedWindow = (selectedWindow + 1)%windowCount;
432 InvalidateRect(switchdialog, NULL, TRUE);
433 }
434 return TRUE;
435 }
436
437 void RotateTasks(BOOL bShift)
438 {
439 HWND hwndFirst, hwndLast;
440 DWORD Size;
441
442 if (windowCount < 2 || !Esc)
443 return;
444
445 hwndFirst = windowList[0];
446 hwndLast = windowList[windowCount - 1];
447
448 if (bShift)
449 {
450 SetWindowPos(hwndLast, HWND_TOP, 0, 0, 0, 0,
451 SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE |
452 SWP_NOOWNERZORDER | SWP_NOREPOSITION);
453
454 MakeWindowActive(hwndLast);
455
456 Size = (windowCount - 1) * sizeof(HWND);
457 MoveMemory(&windowList[1], &windowList[0], Size);
458 windowList[0] = hwndLast;
459 }
460 else
461 {
462 SetWindowPos(hwndFirst, hwndLast, 0, 0, 0, 0,
463 SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE |
464 SWP_NOOWNERZORDER | SWP_NOREPOSITION);
465
466 MakeWindowActive(windowList[1]);
467
468 Size = (windowCount - 1) * sizeof(HWND);
469 MoveMemory(&windowList[0], &windowList[1], Size);
470 windowList[windowCount - 1] = hwndFirst;
471 }
472 }
473
474 VOID
475 DestroyAppWindows(VOID)
476 {
477 // for every item of the icon list:
478 INT i;
479 for (i = 0; i < windowCount; ++i)
480 {
481 // destroy the icon
482 DestroyIcon(iconList[i]);
483 iconList[i] = NULL;
484 }
485 }
486
487 LRESULT WINAPI DoAppSwitch( WPARAM wParam, LPARAM lParam )
488 {
489 HWND hwndActive;
490 MSG msg;
491
492 // FIXME: Is loading timing OK?
493 LoadCoolSwitchSettings();
494
495 if (!CoolSwitch)
496 return 0;
497
498 // Already in the loop.
499 if (switchdialog || Esc) return 0;
500
501 hwndActive = GetActiveWindow();
502 // Nothing is active so exit.
503 if (!hwndActive) return 0;
504
505 if (lParam == VK_ESCAPE)
506 {
507 Esc = TRUE;
508
509 windowCount = 0;
510 EnumWindowsZOrder(EnumerateCallback, 0);
511
512 if (windowCount < 2)
513 return 0;
514
515 RotateTasks(GetAsyncKeyState(VK_SHIFT) < 0);
516
517 hwndActive = GetActiveWindow();
518
519 if (hwndActive == NULL)
520 {
521 Esc = FALSE;
522 return 0;
523 }
524 }
525
526 // Capture current active window.
527 SetCapture( hwndActive );
528
529 switch (lParam)
530 {
531 case VK_TAB:
532 if( !CreateSwitcherWindow(User32Instance) ) goto Exit;
533 if( !GetDialogFont() ) goto Exit;
534 if( !ProcessHotKey() ) goto Exit;
535 break;
536
537 case VK_ESCAPE:
538 break;
539
540 default:
541 goto Exit;
542 }
543 // Main message loop:
544 while (1)
545 {
546 for (;;)
547 {
548 if (PeekMessageW( &msg, 0, 0, 0, PM_NOREMOVE ))
549 {
550 if (!CallMsgFilterW( &msg, MSGF_NEXTWINDOW )) break;
551 /* remove the message from the queue */
552 PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE );
553 }
554 else
555 WaitMessage();
556 }
557
558 switch (msg.message)
559 {
560 case WM_KEYUP:
561 {
562 PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE );
563 if (msg.wParam == VK_MENU)
564 {
565 CompleteSwitch(TRUE);
566 }
567 else if (msg.wParam == VK_RETURN)
568 {
569 CompleteSwitch(TRUE);
570 }
571 else if (msg.wParam == VK_ESCAPE)
572 {
573 TRACE("DoAppSwitch VK_ESCAPE 2\n");
574 CompleteSwitch(FALSE);
575 }
576 goto Exit; //break;
577 }
578
579 case WM_SYSKEYDOWN:
580 {
581 PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE );
582 if (HIWORD(msg.lParam) & KF_ALTDOWN)
583 {
584 if ( msg.wParam == VK_TAB )
585 {
586 if (Esc) break;
587 if (GetKeyState(VK_SHIFT) < 0)
588 {
589 selectedWindow = selectedWindow - 1;
590 if (selectedWindow < 0)
591 selectedWindow = windowCount - 1;
592 }
593 else
594 {
595 selectedWindow = (selectedWindow + 1)%windowCount;
596 }
597 InvalidateRect(switchdialog, NULL, TRUE);
598 }
599 else if ( msg.wParam == VK_ESCAPE )
600 {
601 if (!Esc) break;
602 RotateTasks(GetKeyState(VK_SHIFT) < 0);
603 }
604 }
605 break;
606 }
607
608 case WM_LBUTTONUP:
609 PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE );
610 ProcessMouseMessage(msg.message, msg.lParam);
611 goto Exit;
612
613 default:
614 if (PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE ))
615 {
616 TranslateMessage(&msg);
617 DispatchMessageW(&msg);
618 }
619 break;
620 }
621 }
622 Exit:
623 ReleaseCapture();
624 if (switchdialog) DestroyWindow(switchdialog);
625 if (Esc) DestroyAppWindows();
626 switchdialog = NULL;
627 selectedWindow = 0;
628 windowCount = 0;
629 Esc = FALSE;
630 return 0;
631 }
632
633 //
634 // Switch System Class Window Proc.
635 //
636 LRESULT WINAPI SwitchWndProc_common(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL unicode )
637 {
638 PWND pWnd;
639 PALTTABINFO ati;
640 pWnd = ValidateHwnd(hWnd);
641 if (pWnd)
642 {
643 if (!pWnd->fnid)
644 {
645 NtUserSetWindowFNID(hWnd, FNID_SWITCH);
646 }
647 }
648
649 switch (uMsg)
650 {
651 case WM_NCCREATE:
652 if (!(ati = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*ati))))
653 return 0;
654 SetWindowLongPtrW( hWnd, 0, (LONG_PTR)ati );
655 return TRUE;
656
657 case WM_SHOWWINDOW:
658 if (wParam)
659 {
660 PrepareWindow();
661 ati = (PALTTABINFO)GetWindowLongPtrW(hWnd, 0);
662 ati->cbSize = sizeof(ALTTABINFO);
663 ati->cItems = nItems;
664 ati->cColumns = nCols;
665 ati->cRows = nRows;
666 if (nCols)
667 {
668 ati->iColFocus = (selectedWindow - nShift) % nCols;
669 ati->iRowFocus = (selectedWindow - nShift) / nCols;
670 }
671 else
672 {
673 ati->iColFocus = 0;
674 ati->iRowFocus = 0;
675 }
676 ati->cxItem = CX_ITEM_SPACE;
677 ati->cyItem = CY_ITEM_SPACE;
678 ati->ptStart = ptStart;
679 }
680 return 0;
681
682 case WM_MOUSEMOVE:
683 ProcessMouseMessage(uMsg, lParam);
684 return 0;
685
686 case WM_ACTIVATE:
687 if (wParam == WA_INACTIVE)
688 {
689 CompleteSwitch(FALSE);
690 }
691 return 0;
692
693 case WM_PAINT:
694 OnPaint(hWnd);
695 return 0;
696
697 case WM_DESTROY:
698 isOpen = FALSE;
699 ati = (PALTTABINFO)GetWindowLongPtrW(hWnd, 0);
700 HeapFree( GetProcessHeap(), 0, ati );
701 SetWindowLongPtrW( hWnd, 0, 0 );
702 DestroyAppWindows();
703 NtUserSetWindowFNID(hWnd, FNID_DESTROY);
704 return 0;
705 }
706 return DefWindowProcW(hWnd, uMsg, wParam, lParam);
707 }
708
709 LRESULT WINAPI SwitchWndProcA(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
710 {
711 return SwitchWndProc_common(hWnd, uMsg, wParam, lParam, FALSE);
712 }
713
714 LRESULT WINAPI SwitchWndProcW(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
715 {
716 return SwitchWndProc_common(hWnd, uMsg, wParam, lParam, TRUE);
717 }