/**************************** Module Header ********************************\ * Module Name: mnpopup.c * * Copyright (c) 1985 - 1999, Microsoft Corporation * * Popup Menu Support * * History: * 10-10-90 JimA Cleanup. * 03-18-91 IanJa Window revalidation added \***************************************************************************/ #include "precomp.h" #pragma hdrstop #define RECT_ONLEFT 0 #define RECT_ONTOP 1 #define RECT_ONRIGHT 2 #define RECT_ONBOTTOM 3 #define RECT_ORG 4 BOOL TryRect( UINT wRect, int x, int y, int cx, int cy, LPRECT prcExclude, LPPOINT ppt, PMONITOR pMonitor); /***************************************************************************\ * xxxTrackPopupMenuEx (API) * * Process a popup menu * * Revalidation Notes: * o if pwndOwner is always the owner of the popup menu windows, then we don't * really have to revalidate it: when it is destroyed the popup menu windows * are destroyed first because it owns them - this is detected in MenuWndProc * so we would only have to test pMenuState->fSabotaged. * o pMenuState->fSabotaged must be cleared before this top-level routine * returns, to be ready for next time menus are processed (unless we are * currently inside xxxMenuLoop()) * o pMenuState->fSabotaged should be FALSE when we enter this routine. * o xxxMenuLoop always returns with pMenuState->fSabotaged clear. Use * a UserAssert to verify this. * * History: \***************************************************************************/ int xxxTrackPopupMenuEx( PMENU pMenu, UINT dwFlags, int x, int y, PWND pwndOwner, CONST TPMPARAMS *lpTpm) { PMENUSTATE pMenuState; PWND pwndHierarchy; PPOPUPMENU ppopupMenuHierarchy; LONG sizeHierarchy; int cxPopup, cyPopup; BOOL fSync; int cmd; BOOL fButtonDown; TL tlpwndHierarchy; TL tlpwndT; RECT rcExclude; PTHREADINFO ptiCurrent, ptiOwner; PMONITOR pMonitor; POINT pt; CheckLock(pMenu); CheckLock(pwndOwner); /* * Capture the things we care about in case lpTpm goes away. */ if (lpTpm != NULL) { if (lpTpm->cbSize != sizeof(TPMPARAMS)) { RIPERR0(ERROR_INVALID_PARAMETER, RIP_WARNING, "TrackPopupMenuEx: cbSize is invalid"); return(FALSE); } rcExclude = lpTpm->rcExclude; } ptiCurrent = PtiCurrent(); ptiOwner = GETPTI(pwndOwner); /* * Win95 compatibility: pwndOwner must be owned by ptiCurrent. */ if (ptiCurrent != ptiOwner) { RIPMSG0(RIP_WARNING, "xxxTrackPopupMenuEx: pwndOwner not owned by ptiCurrent"); return FALSE; } UserAssert(pMenu != NULL); if (ptiCurrent->pMenuState != NULL) { if (dwFlags & TPM_RECURSE) { /* * Only allow recursion if: * -The current menu mode is not about to exit * -Both menus notify the same window * -Only one thread is involved in the current menu mode * This will prevent us from getting into some random * scenarios we don't want to deal with */ ppopupMenuHierarchy = ptiCurrent->pMenuState->pGlobalPopupMenu; pwndHierarchy = ppopupMenuHierarchy->spwndNotify; if (ExitMenuLoop(ptiCurrent->pMenuState, ppopupMenuHierarchy) || (pwndHierarchy == NULL) || (pwndHierarchy != pwndOwner) || (ptiCurrent->pMenuState->ptiMenuStateOwner != GETPTI(pwndHierarchy))) { RIPMSG0(RIP_WARNING, "xxxTrackPopupMenuEx: Failing TPM_RECURSE request"); return FALSE; } /* * Terminate any animation */ MNAnimate(ptiCurrent->pMenuState, FALSE); /* * Cancel pending show timer if any. ie, the app wants to * pop up a context menu on a popup before we drop it. */ ppopupMenuHierarchy = ((ppopupMenuHierarchy->spwndActivePopup != NULL) ? ((PMENUWND)(ppopupMenuHierarchy->spwndActivePopup))->ppopupmenu : NULL); if ((ppopupMenuHierarchy != NULL) && ppopupMenuHierarchy->fShowTimer) { _KillTimer(ppopupMenuHierarchy->spwndPopupMenu, IDSYS_MNSHOW); ppopupMenuHierarchy->fShowTimer = FALSE; } /* * If we're currently on a modal menu, let's unlock the capture * so the recursive menu can get it. */ if (!ptiCurrent->pMenuState->fModelessMenu) { ptiCurrent->pq->QF_flags &= ~QF_CAPTURELOCKED; } } else { /* * Allow only one guy to have a popup menu up at a time... */ RIPERR0(ERROR_POPUP_ALREADY_ACTIVE, RIP_VERBOSE, ""); return FALSE; } } // Is button down? if (dwFlags & TPM_RIGHTBUTTON) { fButtonDown = (_GetKeyState(VK_RBUTTON) & 0x8000) != 0; } else { fButtonDown = (_GetKeyState(VK_LBUTTON) & 0x8000) != 0; } /* * Create the menu window. */ pwndHierarchy = xxxNVCreateWindowEx( WS_EX_TOOLWINDOW | WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE, (PLARGE_STRING)MENUCLASS, NULL, WS_POPUP | WS_BORDER, x, y, 100, 100, TestMF(pMenu, MNS_MODELESS) ? pwndOwner : NULL, NULL, (HANDLE)pwndOwner->hModule, NULL, WINVER); if (pwndHierarchy == NULL) { return FALSE; } if (TestWF(pwndOwner, WEFLAYOUTRTL) || (dwFlags & TPM_LAYOUTRTL)) { SetWF(pwndHierarchy, WEFLAYOUTRTL); } // // Do this so that old apps don't get weird borders on tracked popups due // to the app hack used in CreateWindowEx32. // ClrWF(pwndHierarchy, WFOLDUI); ThreadLockAlways(pwndHierarchy, &tlpwndHierarchy); #ifdef HAVE_MN_GETPPOPUPMENU ppopupMenuHierarchy = (PPOPUPMENU)xxxSendMessage(pwndHierarchy, MN_GETPPOPUPMENU, 0, 0); #else ppopupMenuHierarchy = ((PMENUWND)pwndHierarchy)->ppopupmenu; #endif ppopupMenuHierarchy->fDelayedFree = TRUE; Lock(&(ppopupMenuHierarchy->spwndNotify), pwndOwner); LockPopupMenu(ppopupMenuHierarchy, &ppopupMenuHierarchy->spmenu, pMenu); Lock(&(ppopupMenuHierarchy->spwndActivePopup), pwndHierarchy); ppopupMenuHierarchy->ppopupmenuRoot = ppopupMenuHierarchy; ppopupMenuHierarchy->fIsTrackPopup = TRUE; ppopupMenuHierarchy->fFirstClick = fButtonDown; ppopupMenuHierarchy->fRightButton = ((dwFlags & TPM_RIGHTBUTTON) != 0); if (SYSMET(MENUDROPALIGNMENT) || TestMF(pMenu, MFRTL)) { // // popup's below this one need to follow the same direction as // the other menu's on the desktop. // ppopupMenuHierarchy->fDroppedLeft = TRUE; } ppopupMenuHierarchy->fNoNotify = ((dwFlags & TPM_NONOTIFY) != 0); if (fSync = (dwFlags & TPM_RETURNCMD)) ppopupMenuHierarchy->fSynchronous = TRUE; ppopupMenuHierarchy->fIsSysMenu = ((dwFlags & TPM_SYSMENU) != 0); // Set the GlobalPopupMenu variable so that EndMenu works for popupmenus so // that WinWart II people can continue to abuse undocumented functions. // This is nulled out in MNCancel. /* * This is actually needed for cleanup in case this thread ends * execution before we can free the popup. (See xxxDestroyThreadInfo) * * Note that one thread might own pwndOwner and another one might call * TrackPopupMenu (pretty normal if the two threads are attached). So * properly setting (and initializing) pMenuState is a must here. */ pMenuState = xxxMNAllocMenuState(ptiCurrent, ptiOwner, ppopupMenuHierarchy); if (pMenuState == NULL) { /* * Get out. The app never knew we were here so don't notify it */ dwFlags |= TPM_NONOTIFY; goto AbortTrackPopupMenuEx; } /* * Notify the app we are entering menu mode. wParam is 1 since this is a * TrackPopupMenu. */ if (!ppopupMenuHierarchy->fNoNotify) xxxSendMessage(pwndOwner, WM_ENTERMENULOOP, (ppopupMenuHierarchy->fIsSysMenu ? FALSE : TRUE), 0); /* * Send off the WM_INITMENU, set ourselves up for menu mode etc... */ if (!xxxMNStartMenu(ppopupMenuHierarchy, MOUSEHOLD)) { /* * ppopupMenuHierarchy has been destroyed already; let's bail */ goto AbortTrackPopupMenuEx; } /* * If drag and drop, register the window as a target. */ if (pMenuState->fDragAndDrop) { if (!SUCCEEDED(xxxClientRegisterDragDrop(HW(pwndHierarchy)))) { RIPMSG0(RIP_ERROR, "xxxTrackPopupMenuEx: xxxClientRegisterDragDrop failed"); } } if (!ppopupMenuHierarchy->fNoNotify) { ThreadLock(ppopupMenuHierarchy->spwndNotify, &tlpwndT); xxxSendMessage(ppopupMenuHierarchy->spwndNotify, WM_INITMENUPOPUP, (WPARAM)PtoHq(pMenu), MAKELONG(0, (ppopupMenuHierarchy->fIsSysMenu ? 1: 0))); ThreadUnlock(&tlpwndT); ppopupMenuHierarchy->fSendUninit = TRUE; } /* * Size the menu window if needed... */ sizeHierarchy = (LONG)xxxSendMessage(pwndHierarchy, MN_SIZEWINDOW, MNSW_SIZE, 0); if (!sizeHierarchy) { AbortTrackPopupMenuEx: xxxWindowEvent(EVENT_SYSTEM_MENUEND, pwndOwner, OBJID_WINDOW, INDEXID_CONTAINER, 0); /* * Release the mouse capture we set when we called StartMenuState... */ xxxMNReleaseCapture(); /* Notify the app we have exited menu mode. wParam is 1 for real * tracked popups, not sys menu. Check wFlags since ppopupHierarchy * will be gone. */ if (!(dwFlags & TPM_NONOTIFY)) xxxSendMessage(pwndOwner, WM_EXITMENULOOP, ((dwFlags & TPM_SYSMENU) ? FALSE : TRUE), 0L); /* * Make sure we return failure */ fSync = TRUE; cmd = FALSE; goto CleanupTrackPopupMenuEx; } if (glinp.dwFlags & LINP_KEYBOARD) { pMenuState->fUnderline = TRUE; SetMF(pMenu, MFUNDERLINE); } else { ClearMF(pMenu, MFUNDERLINE); } // // Setup popup window dimensions // cxPopup = LOWORD(sizeHierarchy) + 2*SYSMET(CXFIXEDFRAME); cyPopup = HIWORD(sizeHierarchy) + 2*SYSMET(CYFIXEDFRAME); // // Calculate the monitor BEFORE we adjust the point. Otherwise, we might // move the point offscreen. In which case, we will end up pinning the // popup to the primary display, which is wrong. // pt.x = x; pt.y = y; pMonitor = _MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST); // // Horizontal alignment // if (TestWF(pwndOwner, WEFLAYOUTRTL) && !(dwFlags & TPM_CENTERALIGN)) { dwFlags = dwFlags ^ TPM_RIGHTALIGN; } if (dwFlags & TPM_RIGHTALIGN) { #if DBG if (dwFlags & TPM_CENTERALIGN) { RIPMSG0(RIP_WARNING, "TrackPopupMenuEx: TPM_CENTERALIGN ignored"); } #endif // DBG x -= cxPopup; ppopupMenuHierarchy->iDropDir = PAS_LEFT; } else if (dwFlags & TPM_CENTERALIGN) { x -= (cxPopup / 2); } else { ppopupMenuHierarchy->iDropDir = (ppopupMenuHierarchy->fDroppedLeft ? PAS_LEFT : PAS_RIGHT); } // // Vertical alignment // if (dwFlags & TPM_BOTTOMALIGN) { #if DBG if (dwFlags & TPM_VCENTERALIGN) { RIPMSG0(RIP_WARNING, "TrackPopupMenuEx: TPM_VCENTERALIGN ignored"); } #endif // DBG y -= cyPopup; ppopupMenuHierarchy->iDropDir |= PAS_UP; } else if (dwFlags & TPM_VCENTERALIGN) { y -= (cyPopup / 2); } else { ppopupMenuHierarchy->iDropDir |= PAS_DOWN; } /* * If the caller provided an animation direction, use that instead */ if (dwFlags & TPM_ANIMATIONBITS) { ppopupMenuHierarchy->iDropDir = ((dwFlags >> TPM_FIRSTANIBITPOS) & (PAS_VERT | PAS_HORZ)); } // // Get coords to move to. // sizeHierarchy = FindBestPos( x, y, cxPopup, cyPopup, ((lpTpm != NULL) ? &rcExclude : NULL), dwFlags, ppopupMenuHierarchy, pMonitor); if (TestWF(pwndOwner, WEFLAYOUTRTL) && (ppopupMenuHierarchy->iDropDir & PAS_HORZ)) { ppopupMenuHierarchy->iDropDir ^= PAS_HORZ; } /* * If we have an animation direction and the caller wants animation, * set the bit to get it going. */ if ((ppopupMenuHierarchy->iDropDir != 0) && !(dwFlags & TPM_NOANIMATION)) { ppopupMenuHierarchy->iDropDir |= PAS_OUT; } /* * Show the window. Modeless menus are not topmost and get activated. * Modal menus are topmost but don't get activated. */ PlayEventSound(USER_SOUND_MENUPOPUP); xxxSetWindowPos(pwndHierarchy, (pMenuState->fModelessMenu ? PWND_TOP : PWND_TOPMOST), GET_X_LPARAM(sizeHierarchy), GET_Y_LPARAM(sizeHierarchy), 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOOWNERZORDER | (pMenuState->fModelessMenu ? 0 : SWP_NOACTIVATE)); xxxWindowEvent(EVENT_SYSTEM_MENUPOPUPSTART, pwndHierarchy, OBJID_CLIENT, INDEXID_CONTAINER, 0); /* * We need to return TRUE for compatibility w/ async TrackPopupMenu(). * It is conceivable that a menu ID could have ID 0, in which case just * returning the cmd chosen would return FALSE instead of TRUE. */ /* * If mouse is in client of popup, act like clicked down */ pMenuState->fButtonDown = fButtonDown; cmd = xxxMNLoop(ppopupMenuHierarchy, pMenuState, 0, FALSE); /* * If this is a modeless menu, return without clenning up because * the menu is up. */ if (pMenuState->fModelessMenu) { ThreadUnlock(&tlpwndHierarchy); goto ReturnCmdOrTrue; } CleanupTrackPopupMenuEx: if (ThreadUnlock(&tlpwndHierarchy)) { if (!TestWF(pwndHierarchy, WFDESTROYED)) { xxxDestroyWindow(pwndHierarchy); } } if (pMenuState != NULL) { xxxMNEndMenuState (TRUE); } /* * Capture must be unlocked if no menu is active. */ UserAssert(!(ptiCurrent->pq->QF_flags & QF_CAPTURELOCKED) || ((ptiCurrent->pMenuState != NULL) && !ptiCurrent->pMenuState->fModelessMenu)); ReturnCmdOrTrue: return(fSync ? cmd : TRUE); } /***************************************************************************\ * * FindBestPos() * * Gets best point to move popup menu window to, given exclusion area and * screen real estate. Note that for our purposes, we consider center * alignment to be the same as left/top alignment. * * We try to pin the menu to a particular monitor, to avoid having it * cross. * * We try four possibilities if the original position fails. The order of * these is determined by the alignment and "try" flags. Basically, we try * to move the rectangle out of the exclusion area by sliding it horizontally * or vertically without going offscreen. If we can't, then we know that * sliding it in both dimensions will also fail. So we use the original * point, clipping on screen. * * Take the example of a top-left justified popup, which should be moved * horizontally before vertically. We'll try the original point. Then * we'll try to left-justify with the right edge of the exclude rect. Then * we'll try to top-justify with the bottom edge of the exclude rect. Then * we'll try to right-justify with the left edge of the exclude rect. Then * we'll try to bottom-justify with the top edge of the exclude rect. * Finally, we'll use the original pos. * \***************************************************************************/ LONG FindBestPos( int x, int y, int cx, int cy, LPRECT prcExclude, UINT wFlags, PPOPUPMENU ppopupmenu, PMONITOR pMonitor) { int iRect; int iT; UINT awRect[4]; POINT ptT; RECT rcExclude; // // Clip our coords on screen first. We use the same algorithm to clip // as in Win3.1 for dudes with no exclude rect. // if (prcExclude!=NULL) { // Clip exclude rect to monitor! CopyRect(&rcExclude, prcExclude); IntersectRect(&rcExclude, &rcExclude, &pMonitor->rcMonitor); } else { SetRect(&rcExclude, x, y, x, y); } /* * Make sure popup fits completely on the screen * At least the x,y point will be on the screen. */ if (x + cx > pMonitor->rcMonitor.right) { if ((wFlags & TPM_CENTERALIGN) || (x - cx < pMonitor->rcMonitor.left) || (x >= pMonitor->rcMonitor.right)) { x = pMonitor->rcMonitor.right - cx; } else { x -= cx; } if (ppopupmenu->iDropDir & PAS_HORZ) { COPY_FLAG(ppopupmenu->iDropDir, PAS_LEFT, PAS_HORZ); } } if (x < pMonitor->rcMonitor.left) { x += cx; if ((wFlags & TPM_CENTERALIGN) || (x >= pMonitor->rcMonitor.right) || (x < pMonitor->rcMonitor.left)) { x = pMonitor->rcMonitor.left; } if (ppopupmenu->iDropDir & PAS_HORZ) { COPY_FLAG(ppopupmenu->iDropDir, PAS_RIGHT, PAS_HORZ); } } /* * Make sure popup fits completely on the screen * At least the x+cx,y point will be on the screen * for right aligned menus. */ if ((wFlags & TPM_RIGHTALIGN) && (x + cx > pMonitor->rcMonitor.right)) { x = pMonitor->rcMonitor.right - cx; } if (y + cy > pMonitor->rcMonitor.bottom) { if ((wFlags & TPM_VCENTERALIGN) || (y - cy < pMonitor->rcMonitor.top) || (y >= pMonitor->rcMonitor.bottom)) { y = pMonitor->rcMonitor.bottom - cy; } else { y -= cy; } if (ppopupmenu->iDropDir & PAS_VERT) { COPY_FLAG(ppopupmenu->iDropDir, PAS_UP, PAS_VERT); } } if (y < pMonitor->rcMonitor.top) { y += cy; if ((wFlags & TPM_VCENTERALIGN) || (y >= pMonitor->rcMonitor.bottom) || (y < pMonitor->rcMonitor.top)) { y = pMonitor->rcMonitor.top; } if (ppopupmenu->iDropDir & PAS_VERT) { COPY_FLAG(ppopupmenu->iDropDir, PAS_DOWN, PAS_VERT); } } // // Try first point // if (TryRect(RECT_ORG, x, y, cx, cy, &rcExclude, &ptT, pMonitor)) goto FOUND; // // Sort possibilities. Get offset of horizontal rects. // iRect = (wFlags & TPM_VERTICAL) ? 2 : 0; // // Sort horizontally. Note that we treat TPM_CENTERALIGN like // TPM_LEFTALIGN. // // // If we're right-aligned, try to right-align on left side first. // Otherwise, try to left-align on right side first. // iT = (wFlags & TPM_RIGHTALIGN) ? 0 : 2; awRect[0 + iRect] = RECT_ONLEFT + iT; awRect[1 + iRect] = RECT_ONRIGHT - iT; // // Sort vertically. Note that we treat TPM_VCENTERALIGN like // TPM_TOPALIGN. // // If we're bottom-aligned, try to bottom-align with top of rect // first. Otherwise, try to top-align with bottom of exclusion first. // iT = (wFlags & TPM_BOTTOMALIGN) ? 0 : 2; awRect[2 - iRect] = RECT_ONTOP + iT; awRect[3 - iRect] = RECT_ONBOTTOM - iT; // // Loop through sorted alternatives. Note that TryRect fails immediately // if an exclusion coordinate is too close to screen edge. // for (iRect = 0; iRect < 4; iRect++) { if (TryRect(awRect[iRect], x, y, cx, cy, &rcExclude, &ptT, pMonitor)) { switch (awRect[iRect]) { case RECT_ONTOP: ppopupmenu->iDropDir = PAS_UP; break; case RECT_ONLEFT: ppopupmenu->iDropDir = PAS_LEFT; break; case RECT_ONBOTTOM: ppopupmenu->iDropDir = PAS_DOWN; break; case RECT_ONRIGHT: ppopupmenu->iDropDir = PAS_RIGHT; break; } x = ptT.x; y = ptT.y; break; } } FOUND: return MAKELONG(x, y); } /***************************************************************************\ * * TryRect() * * Tries to fit rect on screen without covering exclusion area. Returns * TRUE if success. * \***************************************************************************/ BOOL TryRect( UINT wRect, int x, int y, int cx, int cy, LPRECT prcExclude, LPPOINT ppt, PMONITOR pMonitor) { RECT rcTry; switch (wRect) { case RECT_ONRIGHT: x = prcExclude->right; if (x + cx > pMonitor->rcMonitor.right) return FALSE; break; case RECT_ONBOTTOM: y = prcExclude->bottom; if (y + cy > pMonitor->rcMonitor.bottom) return FALSE; break; case RECT_ONLEFT: x = prcExclude->left - cx; if (x < pMonitor->rcMonitor.left) return FALSE; break; case RECT_ONTOP: y = prcExclude->top - cy; if (y < pMonitor->rcMonitor.top) return FALSE; break; // // case RECT_ORG: // NOP; // break; // } ppt->x = x; ppt->y = y; rcTry.left = x; rcTry.top = y; rcTry.right = x + cx; rcTry.bottom = y + cy; return(!IntersectRect(&rcTry, &rcTry, prcExclude)); }