vbAccelerator - Contents of code file: cTbarMenu.cls

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "cTbarMenu"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

' =========================================================================
' cTbarMenu.cls
'
' vbAccelerator Toolbar control
' Copyright  1998-2000 Steve McMahon (steve@vbaccelerator.com)
'
' Code to implement menu tracking against a toolbar
' rather than a menu bar.
'
' Based on code written by Paul DiLascia for Microsoft Systems
' Journal, C++ Q&A, January 1998.  Converted to VB and
' MDI form support added.
'
' -------------------------------------------------------------------------
' Visit vbAccelerator at http://vbaccelerator.com
' =========================================================================

Private Enum TRACKINGSTATE   '{ // menubar has three states:
   TRACK_NONE = 0 ',   // * normal, not tracking anything
   TRACK_BUTTON ',     // * tracking buttons (F10/Alt mode)
   TRACK_POPUP       '// * tracking popups
End Enum

Private Const CONTROLWIN_BUTTON = &H7FFF&

Private m_iTrackingState As TRACKINGSTATE
Private m_bProcessRightArrow As Boolean
Private m_bProcessLeftArrow  As Boolean
Private m_hMenuTracking As Long
Private m_iPopupTracking As Long
Private m_bEscapeWasPressed As Boolean
Private m_tPMouse As POINTAPI
Private m_iNewPopup As Long

Private m_hWnd As Long
Private m_hWndOwner As Long
Private m_hMenu As Long
Private m_lPtrMenu As Long
Private m_bResetHideInfrequentlyUsed As Boolean
Private m_bAlignMenuLeft As Boolean

Implements ISubclass

Friend Sub CoolMenuAttach(ByVal hWndA As Long, ByVal hWndToolbar As Long, ByVal
 hMenu As Long, ByVal lPtrMenu As Long)
   CoolMenuDetach
   m_hWndOwner = hWndA
   m_hWnd = hWndToolbar
   m_hMenu = hMenu
   m_lPtrMenu = lPtrMenu
   SendMessage m_hWndOwner, WM_ENTERMENULOOP, 0, 0
   AttachMessage Me, m_hWndOwner, WM_MENUSELECT
   AttachMessage Me, m_hWndOwner, WM_DESTROY
End Sub
Friend Sub hMenuChange(ByVal hMenu As Long)
   m_hMenu = hMenu
End Sub
Friend Sub CoolMenuDetach()
   If (m_hWndOwner <> 0) Then
      SendMessage m_hWndOwner, WM_EXITMENULOOP, 0, 0
      DetachMessage Me, m_hWndOwner, WM_MENUSELECT
      DetachMessage Me, m_hWndOwner, WM_DESTROY
      m_hWndOwner = 0
   End If
   m_hWnd = 0
End Sub
Private Property Get GetMDIChildIsMaximized() As Boolean
Dim hwnd As Long
   hwnd = FindWindowEx(m_hWndOwner, 0, "MDIClient", ByVal 0&)
    'GetWindow(m_hWndOwner, GW_CHILD)
   If hwnd <> 0 Then
      hwnd = SendMessageLong(hwnd, WM_MDIGETACTIVE, 0, 0)
      If IsZoomed(hwnd) Then
         GetMDIChildIsMaximized = True
      End If
   End If
End Property

'/////////////////
'// When user selects a new menu item, note whether it has a submenu
'// and/or parent menu, so I know whether right/left arrow should
'// move to the next popup.
'//
Private Sub MenuSelect(ByVal hMenu As Long, ByVal iItem As Long)
   
   If (m_iTrackingState > 0) Then
      '// process right-arrow if item is NOT a submenu
      
      ' SPM: .. and the menu item with the sub-menu is enabled!
      m_bProcessRightArrow = True
      
      Dim tMII As MENUITEMINFO
      Dim lR As Long
      tMII.fMask = MIIM_STATE Or MIIM_SUBMENU
      tMII.cbSize = LenB(tMII)
      lR = GetMenuItemInfo(hMenu, iItem, True, tMII)
      
      If Not (tMII.hSubMenu = 0) Then
         m_bProcessRightArrow = False
         
         'Debug.Print "Checking enabled state"
         'Debug.Print lR, tMII.fState, tMII.hSubMenu
         If ((tMII.fState And MF_DISABLED) = MF_DISABLED) Then
            'Debug.Print "Item is disabled, allow right arrow"
            m_bProcessRightArrow = True
         Else
            'Debug.Print "Item is enabled"
         End If
         
      End If
      '// process left-arrow if curent menu is one I'm tracking
      m_bProcessLeftArrow = (hMenu = m_hMenuTracking)
   End If
   
End Sub


'//////////////////
'// Handle menu input event: Look for left/right to change popup menu,
'// mouse movement over over a different menu button for "hot" popup effect.
'// Returns TRUE if message handled (to eat it).
'//
Friend Function MenuInput(m As msg) As Boolean
Dim iMsg As Long
Dim vKey As Long
Dim tP As POINTAPI
Dim iButton As Long

   'ASSERT_VALID(this);
   Debug.Assert m_iTrackingState = TRACK_POPUP  '; // sanity check
   iMsg = m.message

   If (iMsg = WM_KEYDOWN) Then
      
      '// handle left/right-arow.
      vKey = m.wParam
      If ((vKey = vbKeyLeft And m_bProcessLeftArrow) Or _
         (vKey = vbKeyRight And m_bProcessRightArrow)) Then

         'MBTRACE(_T("CMenuBar::OnMenuInput: handle VK_LEFT/RIGHT\n"));
         CancelMenuAndTrackNewOne _
            GetNextOrPrevButton(m_iPopupTracking, vKey = vbKeyLeft)
         MenuInput = True ' // eat it
      
      ' // escape:
      ElseIf (vKey = vbKeyEscape) Then
         m_bEscapeWasPressed = True ';    // (menu will abort itself)
      End If
      
   ElseIf (iMsg = WM_MOUSEMOVE Or iMsg = WM_LBUTTONDOWN) Then
      '// handle mouse move or click
      LSet tP = m.pt
      ScreenToClient m_hWnd, tP

      If (iMsg = WM_MOUSEMOVE) Then
           ' fixes by James Mihelich.
         ' If the mouse was over a menu button while the menu is displayed
         ' and the user pressed the left or right arrow keys, the menu
         ' changed to the new menu but then immediately moved back
         ' to the menu the mouse was over.  The logic change fixes this
         ' problem.
         iButton = HitTest(tP)
         If (tP.x <> m_tPMouse.x) Or (tP.y <> m_tPMouse.y) Then
            If iButton <> GetHotItem() Then
               If IsValidButton(iButton) Then
                  'Debug.Print "CMTN", iButton
                  If iButton <> m_iPopupTracking Then
                     '// user moved mouse over a different button: track its
                      popup
                     Debug.Print "CMTN" & iButton, m_iPopupTracking
                     CancelMenuAndTrackNewOne iButton
                  End If
               End If
            End If
            LSet m_tPMouse = tP
         End If
      ElseIf iMsg = WM_LBUTTONDOWN Then
         'Debug.Print HitTest(tP)
         If (HitTest(tP) = m_iPopupTracking) Then
            '// user clicked on same button I am tracking: cancel menu
            'MBTRACE(_T("CMenuBar:OnMenuInput: handle mouse click to exit
             popup\n"));
            'Debug.Print "CMTN-1", m_iPopupTracking
            CancelMenuAndTrackNewOne -1
            MenuInput = True ' // eat it
         End If
      End If
   
   End If

End Function

Private Function HitTest(pt As POINTAPI) As Long
Dim iHit As Long
Dim rc As RECT

   'int iHit = CFlatToolBar::HitTest(p);
   'if (iHit>0) {
   '   GetClientRect(&rc);
   '   if (!rc.PtInRect(p)) // if point is outside window
   '      iHit = -1;        // can't be a hit!
   '}
   
   GetClientRect m_hWnd, rc
   If (PtInRect(rc, pt.x, pt.y) = 0) Then
      iHit = -1
      If GetMDIChildIsMaximized Then
         GetRect CONTROLWIN_BUTTON, rc
         If pt.x >= rc.Left And pt.x <= rc.Right Then
            If pt.y >= rc.Top And pt.y <= rc.Bottom Then
               ' In sys menu...
               'Debug.Print "HitTest in SysMenu"
               iHit = CONTROLWIN_BUTTON
            End If
         End If
      End If
   Else
      Dim iT As Long
      iHit = -1
      For iT = 0 To GetButtonCount - 1
         GetRect iT, rc
         If pt.x >= rc.Left And pt.x <= rc.Right Then
            If pt.y >= rc.Top And pt.y <= rc.Bottom Then
               iHit = iT
               Exit For
            End If
         End If
      Next iT
   End If
   HitTest = iHit
   
End Function
Private Property Get IsValidButton(ByVal iButton As Long) As Boolean
   If (iButton > -1) Then
      If GetButtonVisible(iButton) Then
         IsValidButton = True
      End If
   End If
End Property

'//////////////////
'// Cancel the current popup menu by posting WM_CANCELMODE, and track a new
'// menu. iNewPopup is which new popup to track (-1 to quit).
'//
Private Sub CancelMenuAndTrackNewOne(ByVal iNewPopup As Long)

   'MBTRACE(_T("CMenuBar::CancelMenuAndTrackNewOne: %d\n"), iNewPopup);
   'ASSERT_VALID(this);
   If (iNewPopup <> m_iPopupTracking) Then
      PostMessage m_hWndOwner, WM_CANCELMODE, 0, 0 ' // quit menu loop
      m_iNewPopup = iNewPopup                '// go to this popup (-1 = quit)
   End If
End Sub

Private Sub PressButton(ByVal iButton As Long, ByVal bState As Boolean)
Dim tBB As TBBUTTON
Dim fState As Long

   If iButton >= 0 And iButton < GetButtonCount() Then
      SendMessage m_hWnd, TB_GETBUTTON, iButton, tBB
      Dim tBI As TBBUTTONINFO
      tBI.cbSize = LenB(tBI)
      tBI.dwMask = TBIF_STYLE
      SendMessage m_hWnd, TB_GETBUTTONINFO, tBB.idCommand, tBI
      If (tBI.fsStyle And TBSTYLE_DROPDOWN) = TBSTYLE_DROPDOWN Then
         Debug.Print "Not Pressing"
      Else
         fState = Abs(bState)
         SendMessageLong m_hWnd, TB_PRESSBUTTON, tBB.idCommand, fState
      End If
   End If

End Sub

'//////////////////
'// Track the popup submenu associated with the i'th button in the menu bar.
'// This fn actually goes into a loop, tracking different menus until the user
'// selects a command or exits the menu.
'//
Friend Function TrackPopup(ByVal iButton As Long) As Long
Dim nMenuItems As Long
Dim tPM As TPMPARAMS
Dim rcButton As RECT
Dim pt As POINTAPI
Dim hMenuPopup As Long
Dim lR As Long
Dim hwnd As Long
Dim lRtnID As Long
Dim lParam As Long

   Debug.Print "BeginTrackPopup"
   Debug.Assert m_hMenu <> 0
   
   nMenuItems = GetMenuItemCount(m_hMenu)

   Do While (iButton >= 0)                '// while user selects another menu
      
      lRtnID = 0

      m_iNewPopup = -1                '// assume quit after this
      PressButton iButton, True       '// press the button
      UpdateWindow m_hWnd             '// and force repaint now

      '// post a simulated arrow-down into the message stream
      '// so TrackPopupMenu will read it and move to the first item
         ' SPM:  this isn't done in the IE/Office menu bar.
      'PostMessage m_hWndOwner, WM_KEYDOWN, vbKeyDown, 1
      'PostMessage m_hWndOwner, WM_KEYUP, vbKeyDown, 1

      SetTrackingState TRACK_POPUP, iButton '// enter tracking state

      '// Need to install a hook to trap menu input in order to make
      '// left/right-arrow keys and "hot" mouse tracking work.
      '//
      AttachMsgHook Me

      '// get submenu and display it beneath button
      GetRect iButton, rcButton
      ClientRectToScreen m_hWnd, rcButton
      tPM.cbSize = Len(tPM)
      ComputeMenuTrackPoint rcButton, tPM, pt
      
      ' MDI menu:
      If iButton = CONTROLWIN_BUTTON Then
         ' Get the system menu for this MDI client...
         hwnd = GetProp(m_hWnd, "vbalTbar:MDIClient")
         If hwnd <> 0 Then
            hwnd = SendMessageLong(hwnd, WM_MDIGETACTIVE, 0, 0)
            hMenuPopup = GetSystemMenu(hwnd, 0)
         End If
         'Debug.Print "Control Button;"; hWnd, hMenuPopup
         lParam = &HFFFFFFFF
         'Debug.Print hMenuPopup
      Else
         hMenuPopup = GetSubMenu(m_hMenu, iButton)
         lParam = 0
      End If
      If hMenuPopup <> 0 Then
         ' Show the menu:
         m_hMenuTracking = hMenuPopup
         ' Make sure the correct Init popup menu message is called:
         SendMessageLong m_hWndOwner, WM_INITMENUPOPUP, hMenuPopup, lParam
         lR = TrackPopupMenuEx(hMenuPopup, _
            TPM_LEFTALIGN Or TPM_LEFTBUTTON Or TPM_VERTICAL Or TPM_RETURNCMD, _
            pt.x, pt.y, m_hWndOwner, tPM)
         If iButton = CONTROLWIN_BUTTON Then
            SendMessageLong hwnd, WM_SYSCOMMAND, lR, 0
         Else
            'lR is the ID of the menu
            lRtnID = lR
            checkForChevron lRtnID
         End If
      End If
      
      '// uninstall hook.
      DetachMsgHook

      PressButton iButton, False    ';   // un-press button
      UpdateWindow m_hWnd                '// and force repaint now
      
      '// If the user exited the menu loop by pressing Escape,
      '// return to track-button state; otherwise normal non-tracking state.
      If (m_bEscapeWasPressed) Then
         SetTrackingState TRACK_BUTTON, iButton
      Else
         SetTrackingState TRACK_NONE, iButton
      End If
      
      '// If the user moved mouse to a new top-level popup (eg from File to
      '// Edit button), I will have posted a WM_CANCELMODE to quit
      '// the first popup, and set m_iNewPopup to the new menu to show.
      '// Otherwise, m_iNewPopup will be -1 as set above.
      '// So just set iButton to the next popup menu and keep looping...
      iButton = m_iNewPopup
   Loop

   ' Set hot button if mouse is over, otherwise not:
   
   ' The ID of the selected menu
   TrackPopup = lRtnID
   
   If m_bResetHideInfrequentlyUsed Then
      If (m_lPtrMenu <> 0) And (IsWindow(m_hWndOwner) <> 0) Then
         Dim oMenu As Object
         Set oMenu = ObjectFromPtr(m_lPtrMenu)
         oMenu.HideInfrequentlyUsed = True
      End If
   End If
   
   Debug.Print "EndTrackPopup"
   
End Function

Private Sub checkForChevron(ByVal lID As Long)

Dim lIndex As Long
Dim sCaption As String
Dim lClickedIndex As Long
Dim i As Long

   If (m_lPtrMenu <> 0) And (IsWindow(m_hWndOwner) <> 0) Then
      Dim oMenu As Object
      Set oMenu = ObjectFromPtr(m_lPtrMenu)
      
      lIndex = oMenu.ItemForID(lID)
      If (lIndex > 0) Then
         lClickedIndex = lIndex
         sCaption = oMenu.Caption(lIndex)
         If (sCaption = "v-chevron-v") Then
            ' work up towards the parent until we find
            ' the toolbar
            i = lIndex
            Do
               i = oMenu.ItemParentIndex(i)
               If i > 0 Then
                  lIndex = i
                  If oMenu.hMenu(lIndex) = m_hMenu Then
                     ' temporarily show infrequent items:
                     If oMenu.HideInfrequentlyUsed Then
                        oMenu.HideInfrequentlyUsed = False
                        m_bResetHideInfrequentlyUsed = True
                     End If
                     ' then we set the next menu to show to be the current menu
                     ' again:
                     Debug.Print "MENU DECIDED TO DO IT"
                     m_iNewPopup = m_iPopupTracking
                     On Error Resume Next
                     oMenu.ToolbarMenuChevronPress
                     Exit Do
                  End If
               End If
            Loop While i > 0
            
         End If
      End If
   End If
End Sub

Private Sub ComputeMenuTrackPoint(ByRef rc As RECT, tPM As TPMPARAMS, tP As
 POINTAPI)
   Debug.Print rc.Left, rc.Bottom
   If m_bAlignMenuLeft Then
      tP.x = rc.Right
      tP.y = rc.Top
   Else
      tP.x = rc.Left
      tP.y = rc.Bottom
   End If
   LSet tPM.rcExclude = rc
End Sub


Private Sub GetRect(ByVal iButton As Long, ByRef tR As RECT)
Dim tBB As TBBUTTON
Dim iB
   If iButton = CONTROLWIN_BUTTON Then
      ' The system menu?
      iB = 0
   Else
      iB = iButton
   End If
   SendMessage m_hWnd, TB_GETBUTTON, iB, tBB
   SendMessage m_hWnd, TB_GETRECT, tBB.idCommand, tR
   If iButton = CONTROLWIN_BUTTON Then
      OffsetRect tR, -(GetSystemMetrics(SM_CYCAPTION) + 4), 0
      tR.Bottom = tR.Bottom - 2
   End If
End Sub
Private Function GetHotItem() As Long
   GetHotItem = SendMessageLong(m_hWnd, TB_GETHOTITEM, 0, 0)
End Function
Private Function SetHotItem(ByVal iButton As Long) As Long
   Dim tBB As TBBUTTON
   If iButton >= 0 And iButton < GetButtonCount() Then
      SendMessage m_hWnd, TB_GETBUTTON, iButton, tBB
     
      Dim tBI As TBBUTTONINFO
      tBI.cbSize = LenB(tBI)
      tBI.dwMask = TBIF_STYLE
      SendMessage m_hWnd, TB_GETBUTTONINFO, tBB.idCommand, tBI
      If (tBI.fsStyle And TBSTYLE_DROPDOWN) = TBSTYLE_DROPDOWN Then
         SendMessageLong m_hWnd, TB_SETHOTITEM, iButton, 0
      End If
   Else
      SendMessageLong m_hWnd, TB_SETHOTITEM, -1, 0
   End If
   
End Function
Private Function GetButtonVisible(ByVal iButton As Long) As Boolean
Dim tBB As TBBUTTON
Dim bButtonVisible As Boolean
Dim bButtonEnabled As Boolean
Dim tR As RECT
Dim tRT As RECT
   If iButton = CONTROLWIN_BUTTON Then
      GetButtonVisible = GetMDIChildIsMaximized
   Else
      SendMessage m_hWnd, TB_GETBUTTON, iButton, tBB
      bButtonVisible = (SendMessageLong(m_hWnd, TB_ISBUTTONHIDDEN,
       tBB.idCommand, 0) = 0)
      If bButtonVisible Then
         SendMessage m_hWnd, TB_GETRECT, tBB.idCommand, tR
         GetClientRect m_hWnd, tRT
          bButtonVisible = (tR.Right <= tRT.Right) And (tR.Bottom <= tRT.Bottom)
          If (bButtonVisible) Then
            bButtonEnabled = (SendMessageLong(m_hWnd, TB_ISBUTTONENABLED,
             tBB.idCommand, 0) <> 0)
            GetButtonVisible = bButtonEnabled
          End If
      End If
   End If
End Function
Private Function GetButtonCount() As Long
   GetButtonCount = SendMessageLong(m_hWnd, TB_BUTTONCOUNT, 0, 0)
End Function

Private Sub ClientRectToScreen(ByVal hwnd As Long, ByRef tR As RECT)
Dim tP As POINTAPI
   tP.x = tR.Left: tP.y = tR.Top
   ClientToScreen m_hWnd, tP
   tR.Left = tP.x: tR.Top = tP.y
   tP.x = tR.Right: tP.y = tR.Bottom
   ClientToScreen m_hWnd, tP
   tR.Right = tP.x: tR.Bottom = tP.y
End Sub
Private Sub SetTrackingState(ByVal iState As TRACKINGSTATE, ByVal iButton As
 Long)
   If (iState <> m_iTrackingState) Then
      If (iState = TRACK_NONE) Then
         iButton = -1
      End If
'#ifdef _DEBUG
'      static LPCTSTR StateName[] = { _T("NONE"), _T("BUTTON"), _T("POPUP") };
'      MBTRACE(_T("CMenuBar::SetTrackingState to %s, button=%d\n"),
'         StateName[iState], iButton);
'#End If

      SetHotItem iButton              '// could be none (-1)

      If (iState = TRACK_POPUP) Then
         '// set related state stuff
         m_bEscapeWasPressed = False 'FALSE;   // assume Esc key not pressed
         m_bProcessRightArrow = True        '// assume left/right arrow..
         m_bProcessLeftArrow = True         '; // ..will move to prev/next popup
         m_iPopupTracking = iButton          '// which popup I'm tracking
      End If
      m_iTrackingState = iState
   End If
End Sub


Private Function GetNextOrPrevButton(ByVal iButton As Long, ByVal bPrev As
 Boolean) As Long
Dim iSB As Long
Dim bfound As Boolean

   If (bPrev) Then
      iSB = iButton
      Do While Not bfound
         
         If GetMDIChildIsMaximized Then
            
            If iButton = 0 Then
               iButton = CONTROLWIN_BUTTON
            ElseIf iButton >= CONTROLWIN_BUTTON Then
               iButton = GetButtonCount() - 1
            Else
               iButton = iButton - 1
            End If
            
         Else
            iButton = iButton - 1
            If iButton < 0 Then
               iButton = GetButtonCount() - 1
            End If
         End If
         
         If Not (GetButtonVisible(iButton)) Then
            If iButton = iSB Then
               iButton = -1
               Exit Do
            End If
         Else
            bfound = True
         End If
         
      Loop
      
   Else
      iSB = iButton
      Do While Not bfound
         If GetMDIChildIsMaximized Then
            If iButton >= GetButtonCount() And iButton < CONTROLWIN_BUTTON Then
               iButton = CONTROLWIN_BUTTON
            ElseIf iButton = CONTROLWIN_BUTTON Then
               iButton = 0
            Else
               iButton = iButton + 1
            End If
         Else
            iButton = iButton + 1
            If (iButton >= GetButtonCount()) Then
               iButton = 0
            End If
         End If
         
         If Not GetButtonVisible(iButton) Then
            If iButton = iSB Then
               iButton = -1
               Exit Do
            End If
         Else
            bfound = True
         End If
         
      Loop
      
   End If
   'Debug.Print iButton
   GetNextOrPrevButton = iButton
   
End Function
'//////////////////
'// Toggle state from home state to button-tracking and back
'//
Private Sub ToggleTrackButtonMode()
   If (m_iTrackingState = TRACK_NONE Or m_iTrackingState = TRACK_BUTTON) Then
      If m_iTrackingState = TRACK_NONE Then
         SetTrackingState TRACK_BUTTON, 0
      Else
         SetTrackingState TRACK_NONE, 0
     End If
   End If
End Sub

Public Property Let MenuAlignLeft(ByVal bState As Boolean)
   m_bAlignMenuLeft = bState
End Property

Private Sub Class_Initialize()
   debugmsg "cTbarMenu:Initialize"
End Sub

Private Sub Class_Terminate()
   CoolMenuDetach
   debugmsg "cTbarMenu:Terminate"
End Sub

Private Property Let ISubclass_MsgResponse(ByVal RHS As EMsgResponse)
   '
End Property

Private Property Get ISubclass_MsgResponse() As EMsgResponse
   ISubclass_MsgResponse = emrPreprocess
End Property

Private Function ISubclass_WindowProc(ByVal hwnd As Long, ByVal iMsg As Long,
 ByVal wParam As Long, ByVal lParam As Long) As Long
   Select Case iMsg
   Case WM_MENUSELECT
      MenuSelect lParam, (wParam And &HFFFF&)
   Case WM_DESTROY
      CoolMenuDetach
   End Select
End Function