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       ShowWindowAsync(hwnd, SW_RESTORE);
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    GetClassNameW(window, windowText, _countof(windowText));
175    if ((wcscmp(L"Shell_TrayWnd", windowText)==0) ||
176        (wcscmp(L"Progman", windowText)==0) )
177             return TRUE;
178 
179    // First try to get the big icon assigned to the window
180    hIcon = (HICON)SendMessageW(window, WM_GETICON, ICON_BIG, 0);
181    if (!hIcon)
182    {
183       // If no icon is assigned, try to get the icon assigned to the windows' class
184       hIcon = (HICON)GetClassLongPtrW(window, GCL_HICON);
185       if (!hIcon)
186       {
187          // If we still don't have an icon, see if we can do with the small icon,
188          // or a default application icon
189          hIcon = (HICON)SendMessageW(window, WM_GETICON, ICON_SMALL2, 0);
190          if (!hIcon)
191          {
192             // using windows logo icon as default
193             hIcon = gpsi->hIconWindows;
194             if (!hIcon)
195             {
196                //if all attempts to get icon fails go to the next window
197                return TRUE;
198             }
199          }
200       }
201    }
202 
203    windowList[windowCount] = window;
204    iconList[windowCount] = CopyIcon(hIcon);
205 
206    windowCount++;
207 
208    // If we got to the max number of windows,
209    // we won't be able to add any more
210    if(windowCount >= MAX_WINDOWS)
211       return FALSE;
212 
213    return TRUE;
214 }
215 
216 // Function mostly compatible with the normal EnumWindows,
217 // except it lists in Z-Order and it doesn't ensure consistency
218 // if a window is removed while enumerating
219 void EnumWindowsZOrder(WNDENUMPROC callback, LPARAM lParam)
220 {
221     HWND next = GetTopWindow(NULL);
222     while (next != NULL)
223     {
224         if(!callback(next, lParam))
225          break;
226         next = GetWindow(next, GW_HWNDNEXT);
227     }
228 }
229 
230 void ProcessMouseMessage(UINT message, LPARAM lParam)
231 {
232    int xPos = LOWORD(lParam);
233    int yPos = HIWORD(lParam);
234 
235    int xIndex = (xPos - DIALOG_MARGIN) / CX_ITEM_SPACE;
236    int yIndex = (yPos - DIALOG_MARGIN) / CY_ITEM_SPACE;
237 
238    if (xIndex < 0 || nCols <= xIndex ||
239        yIndex < 0 || nRows <= yIndex)
240    {
241         return;
242    }
243 
244    selectedWindow = (yIndex*nCols) + xIndex;
245    if (message == WM_MOUSEMOVE)
246    {
247       InvalidateRect(switchdialog, NULL, TRUE);
248       //RedrawWindow(switchdialog, NULL, NULL, 0);
249    }
250    else
251    {
252       selectedWindow = (yIndex*nCols) + xIndex;
253       CompleteSwitch(TRUE);
254    }
255 }
256 
257 void OnPaint(HWND hWnd)
258 {
259     HDC dialogDC;
260     PAINTSTRUCT paint;
261     RECT cRC, textRC;
262     int i, xPos, yPos, CharCount;
263     HFONT dcFont;
264     HICON hIcon;
265     HPEN hPen;
266     COLORREF Color;
267 
268     // check
269     if (nCols == 0 || nItems == 0)
270         return;
271 
272     // begin painting
273     dialogDC = BeginPaint(hWnd, &paint);
274     if (dialogDC == NULL)
275         return;
276 
277     // fill the client area
278     GetClientRect(hWnd, &cRC);
279     FillRect(dialogDC, &cRC, (HBRUSH)(COLOR_3DFACE + 1));
280 
281     // if the selection index exceeded the display items, then
282     // do display item shifting
283     if (selectedWindow >= nItems)
284         nShift = selectedWindow - nItems + 1;
285     else
286         nShift = 0;
287 
288     for (i = 0; i < nItems; ++i)
289     {
290         // get the icon to display
291         hIcon = iconList[i + nShift];
292 
293         // calculate the position where we start drawing
294         xPos = DIALOG_MARGIN + CX_ITEM_SPACE * (i % nCols) + ITEM_MARGIN;
295         yPos = DIALOG_MARGIN + CY_ITEM_SPACE * (i / nCols) + ITEM_MARGIN;
296 
297         // centering
298         if (nItems < CoolSwitchColumns)
299         {
300             xPos += (itemsW - nItems * CX_ITEM_SPACE) / 2;
301         }
302 
303         // if this position is selected,
304         if (selectedWindow == i + nShift)
305         {
306             // create a solid pen
307             Color = GetSysColor(COLOR_HIGHLIGHT);
308             hPen = CreatePen(PS_SOLID, 1, Color);
309 
310             // draw a rectangle with using the pen
311             SelectObject(dialogDC, hPen);
312             SelectObject(dialogDC, GetStockObject(NULL_BRUSH));
313             Rectangle(dialogDC, xPos, yPos, xPos + CX_ITEM, yPos + CY_ITEM);
314             Rectangle(dialogDC, xPos + 1, yPos + 1,
315                                 xPos + CX_ITEM - 1, yPos + CY_ITEM - 1);
316 
317             // delete the pen
318             DeleteObject(hPen);
319         }
320 
321         // draw icon
322         DrawIconEx(dialogDC, xPos + ICON_MARGIN, yPos + ICON_MARGIN,
323                    hIcon, CX_ICON, CY_ICON, 0, NULL, DI_NORMAL);
324     }
325 
326     // set the text rectangle
327     SetRect(&textRC, DIALOG_MARGIN, DIALOG_MARGIN + itemsH,
328             totalW - DIALOG_MARGIN, totalH - DIALOG_MARGIN);
329 
330     // draw the sunken button around text
331     DrawFrameControl(dialogDC, &textRC, DFC_BUTTON,
332                      DFCS_BUTTONPUSH | DFCS_PUSHED);
333 
334     // get text
335     CharCount = GetWindowTextW(windowList[selectedWindow], windowText,
336                                _countof(windowText));
337 
338     // draw text
339     dcFont = SelectObject(dialogDC, dialogFont);
340     SetTextColor(dialogDC, GetSysColor(COLOR_BTNTEXT));
341     SetBkMode(dialogDC, TRANSPARENT);
342     DrawTextW(dialogDC, windowText, CharCount, &textRC,
343               DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS | DT_SINGLELINE);
344     SelectObject(dialogDC, dcFont);
345 
346     // end painting
347     EndPaint(hWnd, &paint);
348 }
349 
350 DWORD CreateSwitcherWindow(HINSTANCE hInstance)
351 {
352     switchdialog = CreateWindowExW( WS_EX_TOPMOST|WS_EX_DLGMODALFRAME|WS_EX_TOOLWINDOW,
353                                     WC_SWITCH,
354                                     L"",
355                                     WS_POPUP|WS_BORDER|WS_DISABLED,
356                                     CW_USEDEFAULT,
357                                     CW_USEDEFAULT,
358                                     400, 150,
359                                     NULL, NULL,
360                                     hInstance, NULL);
361     if (!switchdialog)
362     {
363        TRACE("[ATbot] Task Switcher Window failed to create.\n");
364        return 0;
365     }
366 
367     isOpen = FALSE;
368     return 1;
369 }
370 
371 DWORD GetDialogFont(VOID)
372 {
373    HDC tDC;
374    TEXTMETRIC tm;
375 
376    dialogFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
377 
378    tDC = GetDC(0);
379    GetTextMetrics(tDC, &tm);
380    fontHeight = tm.tmHeight;
381    ReleaseDC(0, tDC);
382 
383    return 1;
384 }
385 
386 void PrepareWindow(VOID)
387 {
388    nItems = windowCount;
389 
390    nCols = CoolSwitchColumns;
391    nRows = (nItems + CoolSwitchColumns - 1) / CoolSwitchColumns;
392    if (nRows > CoolSwitchRows)
393    {
394       nRows = CoolSwitchRows;
395       nItems = nRows * nCols;
396    }
397 
398    itemsW = nCols * CX_ITEM_SPACE;
399    itemsH = nRows * CY_ITEM_SPACE;
400 
401    totalW = itemsW + 2 * DIALOG_MARGIN;
402    totalH = itemsH + 2 * DIALOG_MARGIN;
403    totalH += fontHeight + 2 * CY_TEXT_MARGIN;
404 
405    ResizeAndCenter(switchdialog, totalW, totalH);
406 }
407 
408 BOOL ProcessHotKey(VOID)
409 {
410    if (!isOpen)
411    {
412       windowCount=0;
413       EnumWindowsZOrder(EnumerateCallback, 0);
414 
415       if (windowCount < 2)
416          return FALSE;
417 
418       selectedWindow = 1;
419 
420       TRACE("[ATbot] HotKey Received. Opening window.\n");
421       ShowWindow(switchdialog, SW_SHOWNORMAL);
422       MakeWindowActive(switchdialog);
423       isOpen = TRUE;
424    }
425    else
426    {
427       TRACE("[ATbot] HotKey Received  Rotating.\n");
428       selectedWindow = (selectedWindow + 1)%windowCount;
429       InvalidateRect(switchdialog, NULL, TRUE);
430    }
431    return TRUE;
432 }
433 
434 void RotateTasks(BOOL bShift)
435 {
436     HWND hwndFirst, hwndLast;
437     DWORD Size;
438 
439     if (windowCount < 2 || !Esc)
440         return;
441 
442     hwndFirst = windowList[0];
443     hwndLast = windowList[windowCount - 1];
444 
445     if (bShift)
446     {
447         SetWindowPos(hwndLast, HWND_TOP, 0, 0, 0, 0,
448                      SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE |
449                      SWP_NOOWNERZORDER | SWP_NOREPOSITION);
450 
451         MakeWindowActive(hwndLast);
452 
453         Size = (windowCount - 1) * sizeof(HWND);
454         MoveMemory(&windowList[1], &windowList[0], Size);
455         windowList[0] = hwndLast;
456     }
457     else
458     {
459         SetWindowPos(hwndFirst, hwndLast, 0, 0, 0, 0,
460                      SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE |
461                      SWP_NOOWNERZORDER | SWP_NOREPOSITION);
462 
463         MakeWindowActive(windowList[1]);
464 
465         Size = (windowCount - 1) * sizeof(HWND);
466         MoveMemory(&windowList[0], &windowList[1], Size);
467         windowList[windowCount - 1] = hwndFirst;
468     }
469 }
470 
471 VOID
472 DestroyAppWindows(VOID)
473 {
474     // for every item of the icon list:
475     INT i;
476     for (i = 0; i < windowCount; ++i)
477     {
478         // destroy the icon
479         DestroyIcon(iconList[i]);
480         iconList[i] = NULL;
481     }
482 }
483 
484 LRESULT WINAPI DoAppSwitch( WPARAM wParam, LPARAM lParam )
485 {
486    HWND hwndActive;
487    MSG msg;
488 
489    // FIXME: Is loading timing OK?
490    LoadCoolSwitchSettings();
491 
492    if (!CoolSwitch)
493       return 0;
494 
495    // Already in the loop.
496    if (switchdialog || Esc) return 0;
497 
498    hwndActive = GetActiveWindow();
499    // Nothing is active so exit.
500    if (!hwndActive) return 0;
501 
502    if (lParam == VK_ESCAPE)
503    {
504       Esc = TRUE;
505 
506       windowCount = 0;
507       EnumWindowsZOrder(EnumerateCallback, 0);
508 
509       if (windowCount < 2)
510           return 0;
511 
512       RotateTasks(GetAsyncKeyState(VK_SHIFT) < 0);
513 
514       hwndActive = GetActiveWindow();
515 
516       if (hwndActive == NULL)
517       {
518           Esc = FALSE;
519           return 0;
520       }
521    }
522 
523    // Capture current active window.
524    SetCapture( hwndActive );
525 
526    switch (lParam)
527    {
528       case VK_TAB:
529          if( !CreateSwitcherWindow(User32Instance) ) goto Exit;
530          if( !GetDialogFont() ) goto Exit;
531          if( !ProcessHotKey() ) goto Exit;
532          break;
533 
534       case VK_ESCAPE:
535          break;
536 
537       default:
538          goto Exit;
539    }
540    // Main message loop:
541    while (1)
542    {
543       for (;;)
544       {
545          if (PeekMessageW( &msg, 0, 0, 0, PM_NOREMOVE ))
546          {
547              if (!CallMsgFilterW( &msg, MSGF_NEXTWINDOW )) break;
548              /* remove the message from the queue */
549              PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE );
550          }
551          else
552              WaitMessage();
553       }
554 
555       switch (msg.message)
556       {
557         case WM_KEYUP:
558         {
559           PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE );
560           if (msg.wParam == VK_MENU)
561           {
562              CompleteSwitch(TRUE);
563           }
564           else if (msg.wParam == VK_RETURN)
565           {
566              CompleteSwitch(TRUE);
567           }
568           else if (msg.wParam == VK_ESCAPE)
569           {
570              TRACE("DoAppSwitch VK_ESCAPE 2\n");
571              CompleteSwitch(FALSE);
572           }
573           goto Exit; //break;
574         }
575 
576         case WM_SYSKEYDOWN:
577         {
578           PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE );
579           if (HIWORD(msg.lParam) & KF_ALTDOWN)
580           {
581              if ( msg.wParam == VK_TAB )
582              {
583                 if (Esc) break;
584                 if (GetKeyState(VK_SHIFT) < 0)
585                 {
586                    selectedWindow = selectedWindow - 1;
587                    if (selectedWindow < 0)
588                       selectedWindow = windowCount - 1;
589                 }
590                 else
591                 {
592                    selectedWindow = (selectedWindow + 1)%windowCount;
593                 }
594                 InvalidateRect(switchdialog, NULL, TRUE);
595              }
596              else if ( msg.wParam == VK_ESCAPE )
597              {
598                 if (!Esc) break;
599                 RotateTasks(GetKeyState(VK_SHIFT) < 0);
600              }
601           }
602           break;
603         }
604 
605         case WM_LBUTTONUP:
606           PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE );
607           ProcessMouseMessage(msg.message, msg.lParam);
608           goto Exit;
609 
610         default:
611           if (PeekMessageW( &msg, 0, msg.message, msg.message, PM_REMOVE ))
612           {
613              TranslateMessage(&msg);
614              DispatchMessageW(&msg);
615           }
616           break;
617       }
618    }
619 Exit:
620    ReleaseCapture();
621    if (switchdialog) DestroyWindow(switchdialog);
622    if (Esc) DestroyAppWindows();
623    switchdialog = NULL;
624    selectedWindow = 0;
625    windowCount = 0;
626    Esc = FALSE;
627    return 0;
628 }
629 
630 //
631 // Switch System Class Window Proc.
632 //
633 LRESULT WINAPI SwitchWndProc_common(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL unicode )
634 {
635    PWND pWnd;
636    PALTTABINFO ati;
637    pWnd = ValidateHwnd(hWnd);
638    if (pWnd)
639    {
640       if (!pWnd->fnid)
641       {
642          NtUserSetWindowFNID(hWnd, FNID_SWITCH);
643       }
644    }
645 
646    switch (uMsg)
647    {
648       case WM_NCCREATE:
649          if (!(ati = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*ati))))
650             return 0;
651          SetWindowLongPtrW( hWnd, 0, (LONG_PTR)ati );
652          return TRUE;
653 
654       case WM_SHOWWINDOW:
655          if (wParam)
656          {
657             PrepareWindow();
658             ati = (PALTTABINFO)GetWindowLongPtrW(hWnd, 0);
659             ati->cbSize = sizeof(ALTTABINFO);
660             ati->cItems = nItems;
661             ati->cColumns = nCols;
662             ati->cRows = nRows;
663             if (nCols)
664             {
665                ati->iColFocus = (selectedWindow - nShift) % nCols;
666                ati->iRowFocus = (selectedWindow - nShift) / nCols;
667             }
668             else
669             {
670                ati->iColFocus = 0;
671                ati->iRowFocus = 0;
672             }
673             ati->cxItem = CX_ITEM_SPACE;
674             ati->cyItem = CY_ITEM_SPACE;
675             ati->ptStart = ptStart;
676          }
677          return 0;
678 
679       case WM_MOUSEMOVE:
680          ProcessMouseMessage(uMsg, lParam);
681          return 0;
682 
683       case WM_ACTIVATE:
684          if (wParam == WA_INACTIVE)
685          {
686             CompleteSwitch(FALSE);
687          }
688          return 0;
689 
690       case WM_PAINT:
691          OnPaint(hWnd);
692          return 0;
693 
694       case WM_DESTROY:
695          isOpen = FALSE;
696          ati = (PALTTABINFO)GetWindowLongPtrW(hWnd, 0);
697          HeapFree( GetProcessHeap(), 0, ati );
698          SetWindowLongPtrW( hWnd, 0, 0 );
699          DestroyAppWindows();
700          NtUserSetWindowFNID(hWnd, FNID_DESTROY);
701          return 0;
702    }
703    return DefWindowProcW(hWnd, uMsg, wParam, lParam);
704 }
705 
706 LRESULT WINAPI SwitchWndProcA(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
707 {
708    return SwitchWndProc_common(hWnd, uMsg, wParam, lParam, FALSE);
709 }
710 
711 LRESULT WINAPI SwitchWndProcW(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
712 {
713    return SwitchWndProc_common(hWnd, uMsg, wParam, lParam, TRUE);
714 }
715