reactos/base/applications/rapps/gui.cpp
2023-05-27 15:38:01 +03:00

823 lines
23 KiB
C++

/*
* PROJECT: ReactOS Applications Manager
* LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
* PURPOSE: GUI classes for RAPPS
* COPYRIGHT: Copyright 2015 David Quintana (gigaherz@gmail.com)
* Copyright 2017 Alexander Shaposhnikov (sanchaez@reactos.org)
* Copyright 2020 He Yang (1160386205@qq.com)
*/
#include "rapps.h"
#include "rosui.h"
#include "crichedit.h"
#include "asyncinet.h"
#include "gui.h"
#include <shlobj_undoc.h>
#include <shlguid_undoc.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atltypes.h>
#include <atlwin.h>
#include <wininet.h>
#include <shellutils.h>
#include <ui/rosctrls.h>
#include <gdiplus.h>
#include <math.h>
#define SEARCH_TIMER_ID 'SR'
#define TREEVIEW_ICON_SIZE 24
// **** CSideTreeView ****
CSideTreeView::CSideTreeView()
: CUiWindow(),
hImageTreeView(ImageList_Create(TREEVIEW_ICON_SIZE, TREEVIEW_ICON_SIZE, GetSystemColorDepth() | ILC_MASK, 0, 1))
{
}
HTREEITEM
CSideTreeView::AddItem(HTREEITEM hParent, CStringW &Text, INT Image, INT SelectedImage, LPARAM lParam)
{
return CUiWindow<CTreeView>::AddItem(hParent, const_cast<LPWSTR>(Text.GetString()), Image, SelectedImage, lParam);
}
HTREEITEM
CSideTreeView::AddCategory(HTREEITEM hRootItem, UINT TextIndex, UINT IconIndex)
{
CStringW szText;
INT Index = 0;
HICON hIcon;
hIcon = (HICON)LoadImageW(
hInst, MAKEINTRESOURCE(IconIndex), IMAGE_ICON, TREEVIEW_ICON_SIZE, TREEVIEW_ICON_SIZE, LR_CREATEDIBSECTION);
if (hIcon)
{
Index = ImageList_AddIcon(hImageTreeView, hIcon);
DestroyIcon(hIcon);
}
szText.LoadStringW(TextIndex);
return AddItem(hRootItem, szText, Index, Index, TextIndex);
}
HIMAGELIST
CSideTreeView::SetImageList()
{
return CUiWindow<CTreeView>::SetImageList(hImageTreeView, TVSIL_NORMAL);
}
VOID
CSideTreeView::DestroyImageList()
{
if (hImageTreeView)
ImageList_Destroy(hImageTreeView);
}
CSideTreeView::~CSideTreeView()
{
DestroyImageList();
}
// **** CSideTreeView ****
// **** CMainWindow ****
CMainWindow::CMainWindow(CAppDB *db, BOOL bAppwiz) : m_ClientPanel(NULL), m_Db(db), bAppwizMode(bAppwiz), SelectedEnumType(ENUM_ALL_INSTALLED)
{
}
CMainWindow::~CMainWindow()
{
LayoutCleanup();
}
VOID
CMainWindow::InitCategoriesList()
{
HTREEITEM hRootItemAvailable;
hRootItemInstalled = m_TreeView->AddCategory(TVI_ROOT, IDS_INSTALLED, IDI_CATEGORY);
m_TreeView->AddCategory(hRootItemInstalled, IDS_APPLICATIONS, IDI_APPS);
m_TreeView->AddCategory(hRootItemInstalled, IDS_UPDATES, IDI_APPUPD);
m_TreeView->AddCategory(TVI_ROOT, IDS_SELECTEDFORINST, IDI_SELECTEDFORINST);
hRootItemAvailable = m_TreeView->AddCategory(TVI_ROOT, IDS_AVAILABLEFORINST, IDI_CATEGORY);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_AUDIO, IDI_CAT_AUDIO);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_VIDEO, IDI_CAT_VIDEO);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_GRAPHICS, IDI_CAT_GRAPHICS);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_GAMES, IDI_CAT_GAMES);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_INTERNET, IDI_CAT_INTERNET);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_OFFICE, IDI_CAT_OFFICE);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_DEVEL, IDI_CAT_DEVEL);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_EDU, IDI_CAT_EDU);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_ENGINEER, IDI_CAT_ENGINEER);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_FINANCE, IDI_CAT_FINANCE);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_SCIENCE, IDI_CAT_SCIENCE);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_TOOLS, IDI_CAT_TOOLS);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_DRIVERS, IDI_CAT_DRIVERS);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_LIBS, IDI_CAT_LIBS);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_THEMES, IDI_CAT_THEMES);
m_TreeView->AddCategory(hRootItemAvailable, IDS_CAT_OTHER, IDI_CAT_OTHER);
m_TreeView->SetImageList();
m_TreeView->Expand(hRootItemInstalled, TVE_EXPAND);
m_TreeView->Expand(hRootItemAvailable, TVE_EXPAND);
m_TreeView->SelectItem(bAppwizMode ? hRootItemInstalled : hRootItemAvailable);
}
BOOL
CMainWindow::CreateStatusBar()
{
m_StatusBar = new CUiWindow<CStatusBar>();
m_StatusBar->m_VerticalAlignment = UiAlign_RightBtm;
m_StatusBar->m_HorizontalAlignment = UiAlign_Stretch;
m_ClientPanel->Children().Append(m_StatusBar);
return m_StatusBar->Create(m_hWnd, (HMENU)IDC_STATUSBAR) != NULL;
}
BOOL
CMainWindow::CreateTreeView()
{
m_TreeView = new CSideTreeView();
m_TreeView->m_VerticalAlignment = UiAlign_Stretch;
m_TreeView->m_HorizontalAlignment = UiAlign_Stretch;
m_VSplitter->First().Append(m_TreeView);
return m_TreeView->Create(m_hWnd) != NULL;
}
BOOL
CMainWindow::CreateApplicationView()
{
m_ApplicationView = new CApplicationView(this); // pass this to ApplicationView for callback purpose
m_ApplicationView->m_VerticalAlignment = UiAlign_Stretch;
m_ApplicationView->m_HorizontalAlignment = UiAlign_Stretch;
m_VSplitter->Second().Append(m_ApplicationView);
return m_ApplicationView->Create(m_hWnd) != NULL;
}
BOOL
CMainWindow::CreateVSplitter()
{
m_VSplitter = new CUiSplitPanel();
m_VSplitter->m_VerticalAlignment = UiAlign_Stretch;
m_VSplitter->m_HorizontalAlignment = UiAlign_Stretch;
m_VSplitter->m_DynamicFirst = FALSE;
m_VSplitter->m_Horizontal = FALSE;
m_VSplitter->m_MinFirst = 0;
// TODO: m_MinSecond should be calculate dynamically instead of hard-coded
m_VSplitter->m_MinSecond = 480;
m_VSplitter->m_Pos = 240;
m_ClientPanel->Children().Append(m_VSplitter);
return m_VSplitter->Create(m_hWnd) != NULL;
}
BOOL
CMainWindow::CreateLayout()
{
BOOL b = TRUE;
bUpdating = TRUE;
m_ClientPanel = new CUiPanel();
m_ClientPanel->m_VerticalAlignment = UiAlign_Stretch;
m_ClientPanel->m_HorizontalAlignment = UiAlign_Stretch;
// Top level
b = b && CreateStatusBar();
b = b && CreateVSplitter();
// Inside V Splitter
b = b && CreateTreeView();
b = b && CreateApplicationView();
if (b)
{
RECT rBottom;
/* Size status bar */
m_StatusBar->SendMessageW(WM_SIZE, 0, 0);
::GetWindowRect(m_StatusBar->m_hWnd, &rBottom);
m_VSplitter->m_Margin.bottom = rBottom.bottom - rBottom.top;
}
bUpdating = FALSE;
return b;
}
VOID
CMainWindow::LayoutCleanup()
{
delete m_TreeView;
delete m_ApplicationView;
delete m_VSplitter;
delete m_StatusBar;
return;
}
BOOL
CMainWindow::InitControls()
{
if (CreateLayout())
{
InitCategoriesList();
UpdateStatusBarText();
return TRUE;
}
return FALSE;
}
VOID
CMainWindow::OnSize(HWND hwnd, WPARAM wParam, LPARAM lParam)
{
if (wParam == SIZE_MINIMIZED)
return;
/* Size status bar */
m_StatusBar->SendMessage(WM_SIZE, 0, 0);
RECT r = {0, 0, LOWORD(lParam), HIWORD(lParam)};
HDWP hdwp = NULL;
INT count = m_ClientPanel->CountSizableChildren();
hdwp = BeginDeferWindowPos(count);
if (hdwp)
{
hdwp = m_ClientPanel->OnParentSize(r, hdwp);
if (hdwp)
{
EndDeferWindowPos(hdwp);
}
}
}
BOOL
CMainWindow::RemoveSelectedAppFromRegistry()
{
if (!IsInstalledEnum(SelectedEnumType))
return FALSE;
CStringW szMsgText, szMsgTitle;
if (!szMsgText.LoadStringW(IDS_APP_REG_REMOVE) || !szMsgTitle.LoadStringW(IDS_INFORMATION))
return FALSE;
if (MessageBoxW(szMsgText, szMsgTitle, MB_YESNO | MB_ICONQUESTION) == IDYES)
{
CAppInfo *InstalledApp = (CAppInfo *)m_ApplicationView->GetFocusedItemData();
if (!InstalledApp)
return FALSE;
return m_Db->RemoveInstalledAppFromRegistry(InstalledApp);
}
return FALSE;
}
BOOL
CMainWindow::UninstallSelectedApp(BOOL bModify)
{
if (!IsInstalledEnum(SelectedEnumType))
return FALSE;
CAppInfo *InstalledApp = (CAppInfo *)m_ApplicationView->GetFocusedItemData();
if (!InstalledApp)
return FALSE;
return InstalledApp->UninstallApplication(bModify);
}
BOOL
CMainWindow::ProcessWindowMessage(HWND hwnd, UINT Msg, WPARAM wParam, LPARAM lParam, LRESULT &theResult, DWORD dwMapId)
{
theResult = 0;
switch (Msg)
{
case WM_CREATE:
if (!InitControls())
::PostMessageW(hwnd, WM_CLOSE, 0, 0);
break;
case WM_DESTROY:
{
ShowWindow(SW_HIDE);
SaveSettings(hwnd, &SettingsInfo);
FreeLogs();
delete m_ClientPanel;
PostQuitMessage(0);
return 0;
}
case WM_COMMAND:
OnCommand(wParam, lParam);
break;
case WM_NOTIFY:
{
LPNMHDR data = (LPNMHDR)lParam;
switch (data->code)
{
case TVN_SELCHANGED:
{
if (data->hwndFrom == m_TreeView->m_hWnd)
{
switch (((LPNMTREEVIEW)lParam)->itemNew.lParam)
{
case IDS_INSTALLED:
UpdateApplicationsList(ENUM_ALL_INSTALLED);
break;
case IDS_APPLICATIONS:
UpdateApplicationsList(ENUM_INSTALLED_APPLICATIONS);
break;
case IDS_UPDATES:
UpdateApplicationsList(ENUM_UPDATES);
break;
case IDS_AVAILABLEFORINST:
UpdateApplicationsList(ENUM_ALL_AVAILABLE);
break;
case IDS_CAT_AUDIO:
UpdateApplicationsList(ENUM_CAT_AUDIO);
break;
case IDS_CAT_DEVEL:
UpdateApplicationsList(ENUM_CAT_DEVEL);
break;
case IDS_CAT_DRIVERS:
UpdateApplicationsList(ENUM_CAT_DRIVERS);
break;
case IDS_CAT_EDU:
UpdateApplicationsList(ENUM_CAT_EDU);
break;
case IDS_CAT_ENGINEER:
UpdateApplicationsList(ENUM_CAT_ENGINEER);
break;
case IDS_CAT_FINANCE:
UpdateApplicationsList(ENUM_CAT_FINANCE);
break;
case IDS_CAT_GAMES:
UpdateApplicationsList(ENUM_CAT_GAMES);
break;
case IDS_CAT_GRAPHICS:
UpdateApplicationsList(ENUM_CAT_GRAPHICS);
break;
case IDS_CAT_INTERNET:
UpdateApplicationsList(ENUM_CAT_INTERNET);
break;
case IDS_CAT_LIBS:
UpdateApplicationsList(ENUM_CAT_LIBS);
break;
case IDS_CAT_OFFICE:
UpdateApplicationsList(ENUM_CAT_OFFICE);
break;
case IDS_CAT_OTHER:
UpdateApplicationsList(ENUM_CAT_OTHER);
break;
case IDS_CAT_SCIENCE:
UpdateApplicationsList(ENUM_CAT_SCIENCE);
break;
case IDS_CAT_TOOLS:
UpdateApplicationsList(ENUM_CAT_TOOLS);
break;
case IDS_CAT_VIDEO:
UpdateApplicationsList(ENUM_CAT_VIDEO);
break;
case IDS_CAT_THEMES:
UpdateApplicationsList(ENUM_CAT_THEMES);
break;
case IDS_SELECTEDFORINST:
UpdateApplicationsList(ENUM_CAT_SELECTED);
break;
}
}
}
break;
}
}
break;
case WM_SIZE:
OnSize(hwnd, wParam, lParam);
break;
case WM_SIZING:
{
LPRECT pRect = (LPRECT)lParam;
if (pRect->right - pRect->left < 565)
pRect->right = pRect->left + 565;
if (pRect->bottom - pRect->top < 300)
pRect->bottom = pRect->top + 300;
return TRUE;
}
case WM_SYSCOLORCHANGE:
{
/* Forward WM_SYSCOLORCHANGE to common controls */
m_ApplicationView->SendMessageW(WM_SYSCOLORCHANGE, wParam, lParam);
m_TreeView->SendMessageW(WM_SYSCOLORCHANGE, wParam, lParam);
}
break;
case WM_TIMER:
if (wParam == SEARCH_TIMER_ID)
{
::KillTimer(hwnd, SEARCH_TIMER_ID);
UpdateApplicationsList(SelectedEnumType);
}
break;
}
return FALSE;
}
VOID
CMainWindow::ShowAboutDlg()
{
CStringW szApp;
CStringW szAuthors;
HICON hIcon;
szApp.LoadStringW(IDS_APPTITLE);
szAuthors.LoadStringW(IDS_APP_AUTHORS);
hIcon = LoadIconW(hInst, MAKEINTRESOURCEW(IDI_MAIN));
ShellAboutW(m_hWnd, szApp, szAuthors, hIcon);
DestroyIcon(hIcon);
}
VOID
CMainWindow::OnCommand(WPARAM wParam, LPARAM lParam)
{
const BOOL bReload = TRUE;
WORD wCommand = LOWORD(wParam);
if (!lParam)
{
switch (wCommand)
{
case ID_SETTINGS:
CreateSettingsDlg(m_hWnd);
break;
case ID_EXIT:
PostMessageW(WM_CLOSE, 0, 0);
break;
case ID_SEARCH:
m_ApplicationView->SetFocusOnSearchBar();
break;
case ID_INSTALL:
if (IsAvailableEnum(SelectedEnumType))
{
if (!m_Selected.IsEmpty())
{
if (DownloadListOfApplications(m_Selected, FALSE))
{
m_Selected.RemoveAll();
UpdateApplicationsList(SelectedEnumType);
}
}
else
{
CAppInfo *App = (CAppInfo *)m_ApplicationView->GetFocusedItemData();
if (App)
{
InstallApplication(App);
}
}
}
break;
case ID_UNINSTALL:
if (UninstallSelectedApp(FALSE))
UpdateApplicationsList(SelectedEnumType, bReload);
break;
case ID_MODIFY:
if (UninstallSelectedApp(TRUE))
UpdateApplicationsList(SelectedEnumType, bReload);
break;
case ID_REGREMOVE:
if (RemoveSelectedAppFromRegistry())
UpdateApplicationsList(SelectedEnumType, bReload);
break;
case ID_REFRESH:
UpdateApplicationsList(SelectedEnumType);
break;
case ID_RESETDB:
m_Db->RemoveCached();
UpdateApplicationsList(SelectedEnumType, bReload);
break;
case ID_HELP:
MessageBoxW(L"Help not implemented yet", NULL, MB_OK);
break;
case ID_ABOUT:
ShowAboutDlg();
break;
case ID_CHECK_ALL:
m_ApplicationView->CheckAll();
break;
case ID_ACTIVATE_APPWIZ:
if (hRootItemInstalled)
m_TreeView->SelectItem(hRootItemInstalled);
break;
}
}
}
VOID
CMainWindow::UpdateStatusBarText()
{
if (m_StatusBar)
{
CStringW szBuffer;
szBuffer.Format(IDS_APPS_COUNT, m_ApplicationView->GetItemCount(), m_Selected.GetCount());
m_StatusBar->SetText(szBuffer);
}
}
VOID
CMainWindow::AddApplicationsToView(CAtlList<CAppInfo *> &List)
{
POSITION CurrentListPosition = List.GetHeadPosition();
while (CurrentListPosition)
{
CAppInfo *Info = List.GetNext(CurrentListPosition);
if (szSearchPattern.IsEmpty() || SearchPatternMatch(Info->szDisplayName, szSearchPattern) ||
SearchPatternMatch(Info->szComments, szSearchPattern))
{
BOOL bSelected = m_Selected.Find(Info) != NULL;
m_ApplicationView->AddApplication(Info, bSelected);
}
}
}
VOID
CMainWindow::UpdateApplicationsList(AppsCategories EnumType, BOOL bReload)
{
bUpdating = TRUE;
if (SelectedEnumType != EnumType)
SelectedEnumType = EnumType;
if (bReload)
m_Selected.RemoveAll();
m_ApplicationView->SetRedraw(FALSE);
if (IsInstalledEnum(EnumType))
{
if (bReload)
m_Db->UpdateInstalled();
// set the display type of application-view. this will remove all the item in application-view too.
m_ApplicationView->SetDisplayAppType(AppViewTypeInstalledApps);
CAtlList<CAppInfo *> List;
m_Db->GetApps(List, EnumType);
AddApplicationsToView(List);
}
else if (IsAvailableEnum(EnumType))
{
if (bReload)
m_Db->UpdateAvailable();
// set the display type of application-view. this will remove all the item in application-view too.
m_ApplicationView->SetDisplayAppType(AppViewTypeAvailableApps);
// enum available softwares
if (EnumType == ENUM_CAT_SELECTED)
{
AddApplicationsToView(m_Selected);
}
else
{
CAtlList<CAppInfo *> List;
m_Db->GetApps(List, EnumType);
AddApplicationsToView(List);
}
}
else
{
ATLASSERT(0 && "This should be unreachable!");
}
m_ApplicationView->SetRedraw(TRUE);
m_ApplicationView->RedrawWindow(0, 0, RDW_INVALIDATE | RDW_ALLCHILDREN); // force the child window to repaint
UpdateStatusBarText();
CStringW text;
if (m_ApplicationView->GetItemCount() == 0 && !szSearchPattern.IsEmpty())
{
text.LoadString(IDS_NO_SEARCH_RESULTS);
}
m_ApplicationView->SetWatermark(text);
bUpdating = FALSE;
}
ATL::CWndClassInfo &
CMainWindow::GetWndClassInfo()
{
DWORD csStyle = CS_VREDRAW | CS_HREDRAW;
static ATL::CWndClassInfo wc = {
{sizeof(WNDCLASSEX), csStyle, StartWindowProc, 0, 0, NULL,
LoadIconW(_AtlBaseModule.GetModuleInstance(), MAKEINTRESOURCEW(IDI_MAIN)), LoadCursorW(NULL, IDC_ARROW),
(HBRUSH)(COLOR_BTNFACE + 1), MAKEINTRESOURCEW(IDR_MAINMENU), szWindowClass, NULL},
NULL,
NULL,
IDC_ARROW,
TRUE,
0,
_T("")};
return wc;
}
HWND
CMainWindow::Create()
{
CStringW szWindowName;
szWindowName.LoadStringW(IDS_APPTITLE);
RECT r = {
(SettingsInfo.bSaveWndPos ? SettingsInfo.Left : CW_USEDEFAULT),
(SettingsInfo.bSaveWndPos ? SettingsInfo.Top : CW_USEDEFAULT),
(SettingsInfo.bSaveWndPos ? SettingsInfo.Width : 680), (SettingsInfo.bSaveWndPos ? SettingsInfo.Height : 450)};
r.right += r.left;
r.bottom += r.top;
return CWindowImpl::Create(
NULL, r, szWindowName.GetString(), WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, WS_EX_WINDOWEDGE);
}
// this function is called when a item of application-view is checked/unchecked
// CallbackParam is the param passed to application-view when adding the item (the one getting focus now).
VOID
CMainWindow::ItemCheckStateChanged(BOOL bChecked, LPVOID CallbackParam)
{
if (bUpdating)
return;
CAppInfo *Info = (CAppInfo *)CallbackParam;
if (bChecked)
{
m_Selected.AddTail(Info);
}
else
{
POSITION Pos = m_Selected.Find(Info);
ATLASSERT(Pos != NULL);
if (Pos != NULL)
{
m_Selected.RemoveAt(Pos);
}
}
UpdateStatusBarText();
}
// this function is called when one or more application(s) should be installed
// if Info is not zero, this app should be installed. otherwise those checked apps should be installed
BOOL
CMainWindow::InstallApplication(CAppInfo *Info)
{
if (Info)
{
if (DownloadApplication(Info))
{
UpdateApplicationsList(SelectedEnumType);
return TRUE;
}
}
return FALSE;
}
BOOL
CMainWindow::SearchTextChanged(CStringW &SearchText)
{
if (szSearchPattern == SearchText)
{
return FALSE;
}
szSearchPattern = SearchText;
DWORD dwDelay = 0;
SystemParametersInfoW(SPI_GETMENUSHOWDELAY, 0, &dwDelay, 0);
SetTimer(SEARCH_TIMER_ID, dwDelay);
return TRUE;
}
void
CMainWindow::HandleTabOrder(int direction)
{
ATL::CSimpleArray<HWND> TabOrderHwndList;
m_TreeView->AppendTabOrderWindow(direction, TabOrderHwndList);
m_ApplicationView->AppendTabOrderWindow(direction, TabOrderHwndList);
if (TabOrderHwndList.GetSize() == 0)
{
// in case the list is empty
return;
}
int FocusIndex;
if ((FocusIndex = TabOrderHwndList.Find(GetFocus())) == -1)
{
FocusIndex = 0; // focus the first window in the list
}
else
{
FocusIndex += direction;
FocusIndex +=
TabOrderHwndList.GetSize(); // FocusIndex might be negative. we don't want to mod a negative number
FocusIndex %= TabOrderHwndList.GetSize();
}
::SetFocus(TabOrderHwndList[FocusIndex]);
return;
}
// **** CMainWindow ****
VOID
MainWindowLoop(CMainWindow *wnd, INT nShowCmd)
{
HACCEL KeyBrd;
MSG Msg;
hMainWnd = wnd->Create();
if (!hMainWnd)
return;
/* Maximize it if we must */
wnd->ShowWindow((SettingsInfo.bSaveWndPos && SettingsInfo.Maximized) ? SW_MAXIMIZE : nShowCmd);
wnd->UpdateWindow();
/* Load the menu hotkeys */
KeyBrd = LoadAcceleratorsW(NULL, MAKEINTRESOURCEW(HOTKEYS));
/* Message Loop */
while (GetMessageW(&Msg, NULL, 0, 0))
{
if (!TranslateAcceleratorW(hMainWnd, KeyBrd, &Msg))
{
if (Msg.message == WM_CHAR && Msg.wParam == VK_TAB)
{
// Move backwards if shift is held down
int direction = (GetKeyState(VK_SHIFT) & 0x8000) ? -1 : 1;
wnd->HandleTabOrder(direction);
continue;
}
TranslateMessage(&Msg);
DispatchMessageW(&Msg);
}
}
}