mirror of
https://github.com/reactos/reactos.git
synced 2024-10-30 11:35:58 +00:00
7e396787ed
Implemented the following actions: stick the window on the left/right or maximize it, with the following shortcuts: - Win key + Arrows; - drag to left/right screen border; - double-click on top/bottom. CORE-12845
632 lines
16 KiB
C
632 lines
16 KiB
C
/*
|
|
* COPYRIGHT: See COPYING in the top level directory
|
|
* PROJECT: ReactOS Win32k subsystem
|
|
* PURPOSE: HotKey support
|
|
* FILE: win32ss/user/ntuser/hotkey.c
|
|
* PROGRAMER: Eric Kohl
|
|
*/
|
|
|
|
/*
|
|
* FIXME: Hotkey notifications are triggered by keyboard input (physical or programatically)
|
|
* and since only desktops on WinSta0 can receive input in seems very wrong to allow
|
|
* windows/threads on destops not belonging to WinSta0 to set hotkeys (receive notifications).
|
|
* -- Gunnar
|
|
*/
|
|
|
|
#include <win32k.h>
|
|
DBG_DEFAULT_CHANNEL(UserHotkey);
|
|
|
|
/* GLOBALS *******************************************************************/
|
|
|
|
/*
|
|
* Hardcoded hotkeys. See http://ivanlef0u.fr/repo/windoz/VI20051005.html
|
|
* or http://repo.meh.or.id/Windows/VI20051005.html .
|
|
*
|
|
* NOTE: The (Shift-)F12 keys are used only for the "UserDebuggerHotKey" setting
|
|
* which enables setting a key shortcut which, when pressed, establishes a
|
|
* breakpoint in the code being debugged:
|
|
* see http://technet.microsoft.com/en-us/library/cc786263(v=ws.10).aspx
|
|
* and http://flylib.com/books/en/4.441.1.33/1/ for more details.
|
|
* By default the key is VK-F12 on a 101-key keyboard, and is VK_SUBTRACT
|
|
* (hyphen / substract sign) on a 82-key keyboard.
|
|
*/
|
|
/* pti pwnd modifiers vk id next */
|
|
// HOT_KEY hkF12 = {NULL, 1, 0, VK_F12, IDHK_F12, NULL};
|
|
// HOT_KEY hkShiftF12 = {NULL, 1, MOD_SHIFT, VK_F12, IDHK_SHIFTF12, &hkF12};
|
|
// HOT_KEY hkWinKey = {NULL, 1, MOD_WIN, 0, IDHK_WINKEY, &hkShiftF12};
|
|
|
|
PHOT_KEY gphkFirst = NULL;
|
|
UINT gfsModOnlyCandidate;
|
|
|
|
/* FUNCTIONS *****************************************************************/
|
|
|
|
VOID FASTCALL
|
|
StartDebugHotKeys(VOID)
|
|
{
|
|
UINT vk = VK_F12;
|
|
UserUnregisterHotKey(PWND_BOTTOM, IDHK_F12);
|
|
UserUnregisterHotKey(PWND_BOTTOM, IDHK_SHIFTF12);
|
|
if (!ENHANCED_KEYBOARD(gKeyboardInfo.KeyboardIdentifier))
|
|
{
|
|
vk = VK_SUBTRACT;
|
|
}
|
|
UserRegisterHotKey(PWND_BOTTOM, IDHK_SHIFTF12, MOD_SHIFT, vk);
|
|
UserRegisterHotKey(PWND_BOTTOM, IDHK_F12, 0, vk);
|
|
TRACE("Start up the debugger hotkeys!! If you see this you eneabled debugprints. Congrats!\n");
|
|
}
|
|
|
|
/*
|
|
* IntGetModifiers
|
|
*
|
|
* Returns a value that indicates if the key is a modifier key, and
|
|
* which one.
|
|
*/
|
|
static
|
|
UINT FASTCALL
|
|
IntGetModifiers(PBYTE pKeyState)
|
|
{
|
|
UINT fModifiers = 0;
|
|
|
|
if (IS_KEY_DOWN(pKeyState, VK_SHIFT))
|
|
fModifiers |= MOD_SHIFT;
|
|
|
|
if (IS_KEY_DOWN(pKeyState, VK_CONTROL))
|
|
fModifiers |= MOD_CONTROL;
|
|
|
|
if (IS_KEY_DOWN(pKeyState, VK_MENU))
|
|
fModifiers |= MOD_ALT;
|
|
|
|
if (IS_KEY_DOWN(pKeyState, VK_LWIN) || IS_KEY_DOWN(pKeyState, VK_RWIN))
|
|
fModifiers |= MOD_WIN;
|
|
|
|
return fModifiers;
|
|
}
|
|
|
|
/*
|
|
* UnregisterWindowHotKeys
|
|
*
|
|
* Removes hotkeys registered by specified window on its cleanup
|
|
*/
|
|
VOID FASTCALL
|
|
UnregisterWindowHotKeys(PWND pWnd)
|
|
{
|
|
PHOT_KEY pHotKey = gphkFirst, phkNext, *pLink = &gphkFirst;
|
|
|
|
while (pHotKey)
|
|
{
|
|
/* Save next ptr for later use */
|
|
phkNext = pHotKey->pNext;
|
|
|
|
/* Should we delete this hotkey? */
|
|
if (pHotKey->pWnd == pWnd)
|
|
{
|
|
/* Update next ptr for previous hotkey and free memory */
|
|
*pLink = phkNext;
|
|
ExFreePoolWithTag(pHotKey, USERTAG_HOTKEY);
|
|
}
|
|
else /* This hotkey will stay, use its next ptr */
|
|
pLink = &pHotKey->pNext;
|
|
|
|
/* Move to the next entry */
|
|
pHotKey = phkNext;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* UnregisterThreadHotKeys
|
|
*
|
|
* Removes hotkeys registered by specified thread on its cleanup
|
|
*/
|
|
VOID FASTCALL
|
|
UnregisterThreadHotKeys(PTHREADINFO pti)
|
|
{
|
|
PHOT_KEY pHotKey = gphkFirst, phkNext, *pLink = &gphkFirst;
|
|
|
|
while (pHotKey)
|
|
{
|
|
/* Save next ptr for later use */
|
|
phkNext = pHotKey->pNext;
|
|
|
|
/* Should we delete this hotkey? */
|
|
if (pHotKey->pti == pti)
|
|
{
|
|
/* Update next ptr for previous hotkey and free memory */
|
|
*pLink = phkNext;
|
|
ExFreePoolWithTag(pHotKey, USERTAG_HOTKEY);
|
|
}
|
|
else /* This hotkey will stay, use its next ptr */
|
|
pLink = &pHotKey->pNext;
|
|
|
|
/* Move to the next entry */
|
|
pHotKey = phkNext;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* IsHotKey
|
|
*
|
|
* Checks if given key and modificators have corresponding hotkey
|
|
*/
|
|
static PHOT_KEY FASTCALL
|
|
IsHotKey(UINT fsModifiers, WORD wVk)
|
|
{
|
|
PHOT_KEY pHotKey = gphkFirst;
|
|
|
|
while (pHotKey)
|
|
{
|
|
if (pHotKey->fsModifiers == fsModifiers &&
|
|
pHotKey->vk == wVk)
|
|
{
|
|
/* We have found it */
|
|
return pHotKey;
|
|
}
|
|
|
|
/* Move to the next entry */
|
|
pHotKey = pHotKey->pNext;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* co_UserProcessHotKeys
|
|
*
|
|
* Sends WM_HOTKEY message if given keys are hotkey
|
|
*/
|
|
BOOL NTAPI
|
|
co_UserProcessHotKeys(WORD wVk, BOOL bIsDown)
|
|
{
|
|
UINT fModifiers;
|
|
PHOT_KEY pHotKey;
|
|
PWND pWnd;
|
|
BOOL DoNotPostMsg = FALSE;
|
|
BOOL IsModifier = FALSE;
|
|
|
|
if (wVk == VK_SHIFT || wVk == VK_CONTROL || wVk == VK_MENU ||
|
|
wVk == VK_LWIN || wVk == VK_RWIN)
|
|
{
|
|
/* Remember that this was a modifier */
|
|
IsModifier = TRUE;
|
|
}
|
|
|
|
fModifiers = IntGetModifiers(gafAsyncKeyState);
|
|
|
|
if (bIsDown)
|
|
{
|
|
if (IsModifier)
|
|
{
|
|
/* Modifier key down -- no hotkey trigger, but remember this */
|
|
gfsModOnlyCandidate = fModifiers;
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
/* Regular key down -- check for hotkey, and reset mod candidates */
|
|
pHotKey = IsHotKey(fModifiers, wVk);
|
|
gfsModOnlyCandidate = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (IsModifier)
|
|
{
|
|
/* Modifier key up -- modifier-only keys are triggered here */
|
|
pHotKey = IsHotKey(gfsModOnlyCandidate, 0);
|
|
gfsModOnlyCandidate = 0;
|
|
}
|
|
else
|
|
{
|
|
/* Regular key up -- no hotkey, but reset mod-only candidates */
|
|
gfsModOnlyCandidate = 0;
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (pHotKey)
|
|
{
|
|
TRACE("Hot key pressed (pWnd %p, id %d)\n", pHotKey->pWnd, pHotKey->id);
|
|
|
|
/* FIXME: See comment about "UserDebuggerHotKey" on top of this file. */
|
|
if (pHotKey->id == IDHK_SHIFTF12 || pHotKey->id == IDHK_F12)
|
|
{
|
|
if (bIsDown)
|
|
{
|
|
ERR("Hot key pressed for Debug Activation! ShiftF12 = %d or F12 = %d\n",pHotKey->id == IDHK_SHIFTF12 , pHotKey->id == IDHK_F12);
|
|
//DoNotPostMsg = co_ActivateDebugger(); // FIXME
|
|
}
|
|
return DoNotPostMsg;
|
|
}
|
|
|
|
/* WIN and F12 keys are not hardcoded here. See comments on top of this file. */
|
|
if (pHotKey->id == IDHK_WINKEY)
|
|
{
|
|
ASSERT(!bIsDown);
|
|
pWnd = ValidateHwndNoErr(InputWindowStation->ShellWindow);
|
|
if (pWnd)
|
|
{
|
|
TRACE("System Hot key Id %d Key %u\n", pHotKey->id, wVk );
|
|
UserPostMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND, SC_TASKLIST, 0);
|
|
co_IntShellHookNotify(HSHELL_TASKMAN, 0, 0);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (pHotKey->id == IDHK_SNAP_LEFT ||
|
|
pHotKey->id == IDHK_SNAP_RIGHT ||
|
|
pHotKey->id == IDHK_SNAP_UP ||
|
|
pHotKey->id == IDHK_SNAP_DOWN)
|
|
{
|
|
HWND topWnd = UserGetForegroundWindow();
|
|
if (topWnd)
|
|
{
|
|
UserPostMessage(topWnd, WM_KEYDOWN, wVk, 0);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
if (!pHotKey->pWnd)
|
|
{
|
|
TRACE("UPTM Hot key Id %d Key %u\n", pHotKey->id, wVk );
|
|
UserPostThreadMessage(pHotKey->pti, WM_HOTKEY, pHotKey->id, MAKELONG(fModifiers, wVk));
|
|
//ptiLastInput = pHotKey->pti;
|
|
return TRUE; /* Don't send any message */
|
|
}
|
|
else
|
|
{
|
|
pWnd = pHotKey->pWnd;
|
|
if (pWnd == PWND_BOTTOM)
|
|
{
|
|
if (gpqForeground == NULL)
|
|
return FALSE;
|
|
|
|
pWnd = gpqForeground->spwndFocus;
|
|
}
|
|
|
|
if (pWnd)
|
|
{
|
|
// pWnd->head.rpdesk->pDeskInfo->spwndShell needs testing.
|
|
if (pWnd == ValidateHwndNoErr(InputWindowStation->ShellWindow) && pHotKey->id == SC_TASKLIST)
|
|
{
|
|
UserPostMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND, SC_TASKLIST, 0);
|
|
co_IntShellHookNotify(HSHELL_TASKMAN, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
TRACE("UPM Hot key Id %d Key %u\n", pHotKey->id, wVk );
|
|
UserPostMessage(UserHMGetHandle(pWnd), WM_HOTKEY, pHotKey->id, MAKELONG(fModifiers, wVk));
|
|
}
|
|
//ptiLastInput = pWnd->head.pti;
|
|
return TRUE; /* Don't send any message */
|
|
}
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
/*
|
|
* DefWndGetHotKey
|
|
*
|
|
* GetHotKey message support
|
|
*/
|
|
UINT FASTCALL
|
|
DefWndGetHotKey(PWND pWnd)
|
|
{
|
|
PHOT_KEY pHotKey = gphkFirst;
|
|
|
|
WARN("DefWndGetHotKey\n");
|
|
|
|
while (pHotKey)
|
|
{
|
|
if (pHotKey->pWnd == pWnd && pHotKey->id == IDHK_REACTOS)
|
|
{
|
|
/* We have found it */
|
|
return MAKELONG(pHotKey->vk, pHotKey->fsModifiers);
|
|
}
|
|
|
|
/* Move to the next entry */
|
|
pHotKey = pHotKey->pNext;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* DefWndSetHotKey
|
|
*
|
|
* SetHotKey message support
|
|
*/
|
|
INT FASTCALL
|
|
DefWndSetHotKey(PWND pWnd, WPARAM wParam)
|
|
{
|
|
UINT fsModifiers, vk;
|
|
PHOT_KEY pHotKey, *pLink;
|
|
INT iRet = 1;
|
|
|
|
WARN("DefWndSetHotKey wParam 0x%x\n", wParam);
|
|
|
|
// A hot key cannot be associated with a child window.
|
|
if (pWnd->style & WS_CHILD)
|
|
return 0;
|
|
|
|
// VK_ESCAPE, VK_SPACE, and VK_TAB are invalid hot keys.
|
|
if (LOWORD(wParam) == VK_ESCAPE ||
|
|
LOWORD(wParam) == VK_SPACE ||
|
|
LOWORD(wParam) == VK_TAB)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
vk = LOWORD(wParam);
|
|
fsModifiers = HIWORD(wParam);
|
|
|
|
if (wParam)
|
|
{
|
|
pHotKey = gphkFirst;
|
|
while (pHotKey)
|
|
{
|
|
if (pHotKey->fsModifiers == fsModifiers &&
|
|
pHotKey->vk == vk &&
|
|
pHotKey->id == IDHK_REACTOS)
|
|
{
|
|
if (pHotKey->pWnd != pWnd)
|
|
iRet = 2; // Another window already has the same hot key.
|
|
break;
|
|
}
|
|
|
|
/* Move to the next entry */
|
|
pHotKey = pHotKey->pNext;
|
|
}
|
|
}
|
|
|
|
pHotKey = gphkFirst;
|
|
pLink = &gphkFirst;
|
|
while (pHotKey)
|
|
{
|
|
if (pHotKey->pWnd == pWnd &&
|
|
pHotKey->id == IDHK_REACTOS)
|
|
{
|
|
/* This window has already hotkey registered */
|
|
break;
|
|
}
|
|
|
|
/* Move to the next entry */
|
|
pLink = &pHotKey->pNext;
|
|
pHotKey = pHotKey->pNext;
|
|
}
|
|
|
|
if (wParam)
|
|
{
|
|
if (!pHotKey)
|
|
{
|
|
/* Create new hotkey */
|
|
pHotKey = ExAllocatePoolWithTag(PagedPool, sizeof(HOT_KEY), USERTAG_HOTKEY);
|
|
if (pHotKey == NULL)
|
|
return 0;
|
|
|
|
pHotKey->pWnd = pWnd;
|
|
pHotKey->id = IDHK_REACTOS; // Don't care, these hot keys are unrelated to the hot keys set by RegisterHotKey
|
|
pHotKey->pNext = gphkFirst;
|
|
gphkFirst = pHotKey;
|
|
}
|
|
|
|
/* A window can only have one hot key. If the window already has a
|
|
hot key associated with it, the new hot key replaces the old one. */
|
|
pHotKey->pti = NULL;
|
|
pHotKey->fsModifiers = fsModifiers;
|
|
pHotKey->vk = vk;
|
|
}
|
|
else if (pHotKey)
|
|
{
|
|
/* Remove hotkey */
|
|
*pLink = pHotKey->pNext;
|
|
ExFreePoolWithTag(pHotKey, USERTAG_HOTKEY);
|
|
}
|
|
|
|
return iRet;
|
|
}
|
|
|
|
|
|
BOOL FASTCALL
|
|
UserRegisterHotKey(PWND pWnd,
|
|
int id,
|
|
UINT fsModifiers,
|
|
UINT vk)
|
|
{
|
|
PHOT_KEY pHotKey;
|
|
PTHREADINFO pHotKeyThread;
|
|
|
|
/* Find hotkey thread */
|
|
if (pWnd == NULL || pWnd == PWND_BOTTOM)
|
|
{
|
|
pHotKeyThread = PsGetCurrentThreadWin32Thread();
|
|
}
|
|
else
|
|
{
|
|
pHotKeyThread = pWnd->head.pti;
|
|
}
|
|
|
|
/* Check for existing hotkey */
|
|
if (IsHotKey(fsModifiers, vk))
|
|
{
|
|
EngSetLastError(ERROR_HOTKEY_ALREADY_REGISTERED);
|
|
WARN("Hotkey already exists\n");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Create new hotkey */
|
|
pHotKey = ExAllocatePoolWithTag(PagedPool, sizeof(HOT_KEY), USERTAG_HOTKEY);
|
|
if (pHotKey == NULL)
|
|
{
|
|
EngSetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
|
return FALSE;
|
|
}
|
|
|
|
pHotKey->pti = pHotKeyThread;
|
|
pHotKey->pWnd = pWnd;
|
|
pHotKey->fsModifiers = fsModifiers;
|
|
pHotKey->vk = vk;
|
|
pHotKey->id = id;
|
|
|
|
/* Insert hotkey to the global list */
|
|
pHotKey->pNext = gphkFirst;
|
|
gphkFirst = pHotKey;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL FASTCALL
|
|
UserUnregisterHotKey(PWND pWnd, int id)
|
|
{
|
|
PHOT_KEY pHotKey = gphkFirst, phkNext, *pLink = &gphkFirst;
|
|
BOOL bRet = FALSE;
|
|
|
|
while (pHotKey)
|
|
{
|
|
/* Save next ptr for later use */
|
|
phkNext = pHotKey->pNext;
|
|
|
|
/* Should we delete this hotkey? */
|
|
if (pHotKey->pWnd == pWnd && pHotKey->id == id)
|
|
{
|
|
/* Update next ptr for previous hotkey and free memory */
|
|
*pLink = phkNext;
|
|
ExFreePoolWithTag(pHotKey, USERTAG_HOTKEY);
|
|
|
|
bRet = TRUE;
|
|
}
|
|
else /* This hotkey will stay, use its next ptr */
|
|
pLink = &pHotKey->pNext;
|
|
|
|
/* Move to the next entry */
|
|
pHotKey = phkNext;
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
|
|
/* SYSCALLS *****************************************************************/
|
|
|
|
|
|
BOOL APIENTRY
|
|
NtUserRegisterHotKey(HWND hWnd,
|
|
int id,
|
|
UINT fsModifiers,
|
|
UINT vk)
|
|
{
|
|
PHOT_KEY pHotKey;
|
|
PWND pWnd = NULL;
|
|
PTHREADINFO pHotKeyThread;
|
|
BOOL bRet = FALSE;
|
|
|
|
TRACE("Enter NtUserRegisterHotKey\n");
|
|
|
|
if (fsModifiers & ~(MOD_ALT|MOD_CONTROL|MOD_SHIFT|MOD_WIN)) // FIXME: Does Win2k3 support MOD_NOREPEAT?
|
|
{
|
|
WARN("Invalid modifiers: %x\n", fsModifiers);
|
|
EngSetLastError(ERROR_INVALID_FLAGS);
|
|
return 0;
|
|
}
|
|
|
|
UserEnterExclusive();
|
|
|
|
/* Find hotkey thread */
|
|
if (hWnd == NULL)
|
|
{
|
|
pHotKeyThread = gptiCurrent;
|
|
}
|
|
else
|
|
{
|
|
pWnd = UserGetWindowObject(hWnd);
|
|
if (!pWnd)
|
|
goto cleanup;
|
|
|
|
pHotKeyThread = pWnd->head.pti;
|
|
|
|
/* Fix wine msg "Window on another thread" test_hotkey */
|
|
if (pWnd->head.pti != gptiCurrent)
|
|
{
|
|
EngSetLastError(ERROR_WINDOW_OF_OTHER_THREAD);
|
|
WARN("Must be from the same Thread.\n");
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/* Check for existing hotkey */
|
|
if (IsHotKey(fsModifiers, vk))
|
|
{
|
|
EngSetLastError(ERROR_HOTKEY_ALREADY_REGISTERED);
|
|
WARN("Hotkey already exists\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Create new hotkey */
|
|
pHotKey = ExAllocatePoolWithTag(PagedPool, sizeof(HOT_KEY), USERTAG_HOTKEY);
|
|
if (pHotKey == NULL)
|
|
{
|
|
EngSetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
|
goto cleanup;
|
|
}
|
|
|
|
pHotKey->pti = pHotKeyThread;
|
|
pHotKey->pWnd = pWnd;
|
|
pHotKey->fsModifiers = fsModifiers;
|
|
pHotKey->vk = vk;
|
|
pHotKey->id = id;
|
|
|
|
/* Insert hotkey to the global list */
|
|
pHotKey->pNext = gphkFirst;
|
|
gphkFirst = pHotKey;
|
|
|
|
bRet = TRUE;
|
|
|
|
cleanup:
|
|
TRACE("Leave NtUserRegisterHotKey, ret=%i\n", bRet);
|
|
UserLeave();
|
|
return bRet;
|
|
}
|
|
|
|
|
|
BOOL APIENTRY
|
|
NtUserUnregisterHotKey(HWND hWnd, int id)
|
|
{
|
|
PHOT_KEY pHotKey = gphkFirst, phkNext, *pLink = &gphkFirst;
|
|
BOOL bRet = FALSE;
|
|
PWND pWnd = NULL;
|
|
|
|
TRACE("Enter NtUserUnregisterHotKey\n");
|
|
UserEnterExclusive();
|
|
|
|
/* Fail if given window is invalid */
|
|
if (hWnd && !(pWnd = UserGetWindowObject(hWnd)))
|
|
goto cleanup;
|
|
|
|
while (pHotKey)
|
|
{
|
|
/* Save next ptr for later use */
|
|
phkNext = pHotKey->pNext;
|
|
|
|
/* Should we delete this hotkey? */
|
|
if (pHotKey->pWnd == pWnd && pHotKey->id == id)
|
|
{
|
|
/* Update next ptr for previous hotkey and free memory */
|
|
*pLink = phkNext;
|
|
ExFreePoolWithTag(pHotKey, USERTAG_HOTKEY);
|
|
|
|
bRet = TRUE;
|
|
}
|
|
else /* This hotkey will stay, use its next ptr */
|
|
pLink = &pHotKey->pNext;
|
|
|
|
/* Move to the next entry */
|
|
pHotKey = phkNext;
|
|
}
|
|
|
|
cleanup:
|
|
TRACE("Leave NtUserUnregisterHotKey, ret=%i\n", bRet);
|
|
UserLeave();
|
|
return bRet;
|
|
}
|
|
|
|
/* EOF */
|