/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * The contents of this file are subject to the Netscape Public License * Version 1.0 (the "NPL"); you may not use this file except in * compliance with the NPL. You may obtain a copy of the NPL at * http://www.mozilla.org/NPL/ * * Software distributed under the NPL is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL * for the specific language governing rights and limitations under the * NPL. * * The Initial Developer of this code under the NPL is Netscape * Communications Corporation. Portions created by Netscape are * Copyright (C) 1998 Netscape Communications Corporation. All Rights * Reserved. */ /* Features (not including the look/feel/behavior from inherited classes): (1) LMenu based. This allows flexible use of the class for both resource-based menus and menus built dynamically. Additionally, the benefits of attaching commands to menu items (using Mcmd and/or dynamically) is gained. (2) The "current item" can either be marked or not. If marking is specified, a mark character can be specified as well. (3) Popup or Popdown behavior. (4) A delay before popup value is supported. This gives rise to the "quick click" feature described below. (5) Quick clicks are supported (ala Greg's Browser and others). This allows a value or command to be specified such that if the button is released before the popup delay value, either the value will change to the specified value or the specified command will be sent. Note that if the quick click is command-based, the command does not have to coincide with a menu command for a menu item (it can be any arbitrary command). Quick click commands are supported even when there is no menu (as long as the quick click command is an enabled command). (6) Use of factory methods for menu management allows subclasses to add menu caching, etc. (7) Use of template methods (or hooks) for handling "quick clicks" and value changes allow subclasses to further customize the behavior. For example, a subclass may implement a "smart" button which will perform an action directly (instead of relying on broadcaster/listener delegation) by overriding HandleNewValue. For example, CGuideMenuPopup (which is a subclass of CPatternButtonPopup) can be inserted into a PPob window resource and It Will Just Workª without additional code to link broadcasters to listeners and additional ListenToMessage cases sprinkled about. (8) Fully configurable through Constructor PPobs and accessor methods. CAVEAT: Although we do correctly save and restore the system font, if Mercutio MDEF is being used in the app, then it must be used in all menus which change the system font in order to not mess up the system font. It is probably a wise idea to use Mercutio MDEF in *all* menus throughout the app to be safe. */ #include "CPatternButtonPopup.h" #include #include "StSetBroadcasting.h" #include "CDrawingState.h" #include "CApplicationEventAttachment.h" #include "CTargetedUpdateMenuRegistry.h" // --------------------------------------------------------------------------- // ¥ CPatternButtonPopup // --------------------------------------------------------------------------- // Stream-based ctor CPatternButtonPopup::CPatternButtonPopup( LStream* inStream) : mMenu(nil), mPopupMenuID(0), mPopupTextTraitsID(0), mInitialCurrentItem(0), mTicksBeforeDisplayingPopup(9), mQuickClickValueOrCommand(0), mQuickClickIsCommandBased(false), mResourceBasedMenu(false), mPopdownBehavior(false), mMarkCurrentItem(false), mMarkCharacter(0), mDetachResource(false), mOwnsMenu(false), mPopUpMenuSelectWasCalled(true), // Start out true to ensure proper behavior // even if super::HandlePopupMenuSelect() // is called instead of our own super(inStream) { // Read in the data from the stream ThrowIfNil_(inStream); LStream& theStream = *inStream; theStream >> mPopupMenuID; theStream >> mPopupTextTraitsID; theStream >> mInitialCurrentItem; theStream >> mTicksBeforeDisplayingPopup; theStream >> mQuickClickValueOrCommand; theStream >> mQuickClickIsCommandBased; theStream >> mResourceBasedMenu; theStream >> mPopdownBehavior; theStream >> mMarkCurrentItem; theStream >> mMarkCharacter; theStream >> mDetachResource; // Ignore the ticks before displaying popup value specified in the resource. // That was a mistake. mTicksBeforeDisplayingPopup = GetDblTime(); } // --------------------------------------------------------------------------- // ¥ ~CPatternButtonPopup // --------------------------------------------------------------------------- // dtor CPatternButtonPopup::~CPatternButtonPopup() { CPatternButtonPopup::EliminatePreviousMenu(); } #pragma mark - // --------------------------------------------------------------------------- // ¥ FinishCreateSelf // --------------------------------------------------------------------------- void CPatternButtonPopup::FinishCreateSelf() { super::FinishCreateSelf(); MakeNewMenu(); } #pragma mark - // --------------------------------------------------------------------------- // ¥ GetMenu // --------------------------------------------------------------------------- // The menu factory methods (GetMenu, OwnsMenu, SetMenu, AdoptMenu, // MakeNewMenu, and EliminatePreviousMenu) *must* be overriden as a group. // The main reason for overriding this group of methods would be to // do some form of menu caching. LMenu* CPatternButtonPopup::GetMenu() const { return mMenu; } // --------------------------------------------------------------------------- // ¥ OwnsMenu // --------------------------------------------------------------------------- // The menu factory methods (GetMenu, OwnsMenu, SetMenu, AdoptMenu, // MakeNewMenu, and EliminatePreviousMenu) *must* be overriden as a group. // The main reason for overriding this group of methods would be to // do some form of menu caching. Boolean CPatternButtonPopup::OwnsMenu() const { return mOwnsMenu; } // --------------------------------------------------------------------------- // ¥ SetMenu // --------------------------------------------------------------------------- // The menu factory methods (GetMenu, OwnsMenu, SetMenu, AdoptMenu, // MakeNewMenu, and EliminatePreviousMenu) *must* be overriden as a group. // The main reason for overriding this group of methods would be to // do some form of menu caching. void CPatternButtonPopup::SetMenu(LMenu* inMenu) { EliminatePreviousMenu(); mMenu = inMenu; mOwnsMenu = false; } // --------------------------------------------------------------------------- // ¥ AdoptMenu // --------------------------------------------------------------------------- // The menu factory methods (GetMenu, OwnsMenu, SetMenu, AdoptMenu, // MakeNewMenu, and EliminatePreviousMenu) *must* be overriden as a group. // The main reason for overriding this group of methods would be to // do some form of menu caching. void CPatternButtonPopup::AdoptMenu( LMenu* inMenuToAdopt) { EliminatePreviousMenu(); mMenu = inMenuToAdopt; mOwnsMenu = true; } // --------------------------------------------------------------------------- // ¥ MakeNewMenu // --------------------------------------------------------------------------- // The menu factory methods (GetMenu, OwnsMenu, SetMenu, AdoptMenu, // MakeNewMenu, and EliminatePreviousMenu) *must* be overriden as a group. // The main reason for overriding this group of methods would be to // do some form of menu caching. void CPatternButtonPopup::MakeNewMenu() { // Create popup menu if appropriate if (mResourceBasedMenu) { if (mPopupMenuID) { // Resource-based menu (via ::GetMenu) AdoptMenu(new LMenu(mPopupMenuID)); mValue = 0; SetPopupMinMaxValues(); // Detach the resource as specified if (mDetachResource) { ::DetachResource((Handle) GetMenu()->GetMacMenuH()); } // For resource-based menus, set the initial current item StSetBroadcasting setBroadcasting(this, false); try { SetValue(mInitialCurrentItem); } catch (const CValueRangeException&) { } catch (const CAttemptToSetDisabledValueException&) { } } } else { if (mPopupMenuID) { // Dynamic menu (via ::NewMenu) AdoptMenu(new LMenu(mPopupMenuID, "\p")); mValue = 0; SetPopupMinMaxValues(); } } } // --------------------------------------------------------------------------- // ¥ EliminatePreviousMenu // --------------------------------------------------------------------------- // The menu factory methods (GetMenu, OwnsMenu, SetMenu, AdoptMenu, // MakeNewMenu, and EliminatePreviousMenu) *must* be overriden as a group. // The main reason for overriding this group of methods would be to // do some form of menu caching. void CPatternButtonPopup::EliminatePreviousMenu() { if (mMenu) { if (mOwnsMenu) { delete mMenu; } mMenu = nil; } } #pragma mark - // --------------------------------------------------------------------------- // ¥ TrackHotSpot // --------------------------------------------------------------------------- Boolean CPatternButtonPopup::TrackHotSpot( Int16 inHotSpot, Point inPoint, Int16 inModifiers) { Boolean pointInHotSpot; // The value we set mPopUpMenuSelectWasCalled // is not important. We just want to restore // the value when this function returns. StValueChanger theValueChanger(mPopUpMenuSelectWasCalled, true); // If no menu, then allow normal tracking and // perform a quick click if button is released // in the hot spot. if (!GetMenu() || !GetMenu()->GetMacMenuH()) { pointInHotSpot = super::TrackHotSpot(inHotSpot, inPoint, inModifiers); if (pointInHotSpot) { if (mQuickClickIsCommandBased) { HandleQuickClick(); } } return pointInHotSpot; } Point currPt = inPoint; UInt32 endTicks = sWhenLastMouseDown + mTicksBeforeDisplayingPopup; // For the initial mouse down, the // mouse is currently inside the HotSpot // when it was previously outside Boolean currInside = true; Boolean prevInside = false; HotSpotAction(inHotSpot, currInside, prevInside); // If the controlKey is down then we bypass quick click tracking in favor of // the popup menu. Additionally, we don't even bother tracking unless // mQuickClickValueOrCommand is non-zero if (!CApplicationEventAttachment::CurrentEventHasModifiers(controlKey) && mQuickClickValueOrCommand) { // Track the mouse while it is down Point currPt = inPoint; while (::StillDown() && ::TickCount() < endTicks) { ::GetMouse(&currPt); // Must keep track if mouse moves from prevInside = currInside; // In-to-Out or Out-To-In currInside = PointInHotSpot(currPt, inHotSpot); if (!currInside) { // Reset endTicks if mouse moves outside endTicks = ::TickCount() + mTicksBeforeDisplayingPopup; } else { // Quick Click tracking if (!::StillDown()) { // Set button in "up" state HotSpotAction(inHotSpot, false, true); // Check if MouseUp occurred in HotSpot ::GetMouse(&currPt); pointInHotSpot = PointInHotSpot(currPt, inHotSpot); if (pointInHotSpot) { HandleQuickClick(); } return pointInHotSpot; } } HotSpotAction(inHotSpot, currInside, prevInside); } } if (!::StillDown()) { // Set button in "up" state HotSpotAction(inHotSpot, false, true); // Check if MouseUp occurred in HotSpot ::GetMouse(&currPt); pointInHotSpot = PointInHotSpot(currPt, inHotSpot); if (pointInHotSpot) { HandleQuickClick(); } return pointInHotSpot; } // We tracked inside the button longer than // mTicksBeforeDisplayingPopup and the mouse is // still down, so we now skip normal tracking // to display the popup menu Int16 menuID = 0; Int16 menuItem = GetValue(); Point popLocation; GetPopupMenuPosition(popLocation); // Handle user interaction with the menu HandlePopupMenuSelect(popLocation, mPopdownBehavior ? 1 : menuItem, menuID, menuItem); if (!mPopUpMenuSelectWasCalled && mQuickClickValueOrCommand) { // If the popup menu was not displayed, // then we will check once more if MouseUp // occurred in HotSpot so we can do // the quick click ::GetMouse(&currPt); pointInHotSpot = PointInHotSpot(currPt, inHotSpot); if (pointInHotSpot) { HandleQuickClick(); } } Rect localFrame; Point mousePt; FocusDraw(); CalcLocalFrameRect(localFrame); ::GetMouse(&mousePt); mMouseInFrame = ::PtInRect(mousePt, &localFrame); // Set the new item, if specified. NOTE: We set the // value here so that this control acts the same as // LStdControl and LGAPopup if (menuItem > 0) { try { SetValue(menuItem); } catch (const CValueRangeException&) { } catch (const CAttemptToSetDisabledValueException&) { } } else { // Set button in "up" state HotSpotAction(inHotSpot, false, true); } return (menuItem > 0); } // --------------------------------------------------------------------------- // ¥ HandlePopupMenuSelect // --------------------------------------------------------------------------- // Handle user interaction with the popup menu void CPatternButtonPopup::HandlePopupMenuSelect( Point inPopupLoc, Int16 inCurrentItem, Int16& outMenuID, Int16& outMenuItem) { ThrowIfNil_(GetMenu()); ThrowIfNil_(GetMenu()->GetMacMenuH()); mPopUpMenuSelectWasCalled = false; // Handle the actual insertion into the hierarchical menubar. // // From Apple Sample Code (PopupMenuWithCurFont.c): // "Believe it or not, it's important to insert // the menu before diddling the font characteristics." // // Note that this is also what LGAPopup.cp does. Unfortunately, // neither this nor the StMercutioMDEFTextState seems to help // with the font metrics problems we see with Mercurtio MDEF. ::InsertMenu(GetMenu()->GetMacMenuH(), hierMenu); Int16 saveFont = ::LMGetSysFontFam(); Int16 saveSize = ::LMGetSysFontSize(); StMercutioMDEFTextState theMercutioMDEFTextState; try { // Reconfigure the system font so that the menu will be drawn in our desired // font and size. if (mPopupTextTraitsID) { FocusDraw(); TextTraitsH traitsH = UTextTraits::LoadTextTraits(mPopupTextTraitsID); if (traitsH) { // Bug #64133 kellys // If setting to application font, get the application font for current script if((**traitsH).fontNumber == 1) ::LMSetSysFontFam ( ::GetScriptVariable(::FontToScript(1), smScriptAppFond) ); else ::LMSetSysFontFam ( (**traitsH).fontNumber ); ::LMSetSysFontSize((**traitsH).size); ::LMSetLastSPExtra(-1L); } } // Adjust the contents of the menu AdjustMenuContents(); // Setup the currently selected menu item SetupCurrentMenuItem(GetMenu()->GetMacMenuH(), GetValue()); // Call PopupMenuSelect and wait for it to return Int32 result = 0; if (::StillDown()) { mPopUpMenuSelectWasCalled = true; result = ::PopUpMenuSelect( GetMenu()->GetMacMenuH(), inPopupLoc.v, inPopupLoc.h, inCurrentItem); } // Extract the values from the returned result these are then passed // back out to the caller outMenuID = HiWord(result); outMenuItem = LoWord(result); } catch (...) { // Ignore errors } // Restore the system font ::LMSetSysFontFam(saveFont); ::LMSetSysFontSize(saveSize); ::LMSetLastSPExtra(-1L); // Finally get the menu removed ::DeleteMenu(mPopupMenuID); } // --------------------------------------------------------------------------- // ¥ HotSpotResult // --------------------------------------------------------------------------- // Note that actual hot spot result processing occurs in TrackHotSpot. This // is because the information necessary to perform the correct action is // only available while tracking. void CPatternButtonPopup::HotSpotResult(Int16 inHotSpot) { if (!IsBehaviourToggle()) { // Make sure that non-toggle buttons return to the "up" state HotSpotAction(inHotSpot, false, true); } } #pragma mark - // --------------------------------------------------------------------------- // ¥ AdjustMenuContents // --------------------------------------------------------------------------- // Do stuff to the menu (add or remove menu items). The default implementation // is intentionally a noop. Override to add interesting behavior. void CPatternButtonPopup::AdjustMenuContents() { } // --------------------------------------------------------------------------- // ¥ SetupCurrentMenuItem // --------------------------------------------------------------------------- // Mark new item and unmark old item void CPatternButtonPopup::SetupCurrentMenuItem(MenuHandle inMenuH, Int16 inCurrentItem) { ThrowIfNil_(inMenuH); if (mMarkCurrentItem) { if (GetValue() != inCurrentItem) { Int16 oldItem = GetValue(); if (oldItem > 0) { ::SetItemMark(inMenuH, oldItem, noMark); } } if (mMarkCharacter && (inCurrentItem > 0)) { ::SetItemMark(inMenuH, inCurrentItem, mMarkCharacter); } else { ::SetItemMark(inMenuH, inCurrentItem, noMark); } } } // --------------------------------------------------------------------------- // ¥ GetPopupMenuPosition // --------------------------------------------------------------------------- // Get the position for displaying the popup menu in global coordinates void CPatternButtonPopup::GetPopupMenuPosition(Point &outPopupLoc) { Rect popupRect; CalcLocalFrameRect(popupRect); if (mPopdownBehavior) { outPopupLoc.v = popupRect.bottom + 1; outPopupLoc.h = popupRect.left + 2; } else { outPopupLoc.v = popupRect.top; outPopupLoc.h = popupRect.left; } LocalToPortPoint(outPopupLoc); PortToGlobalPoint(outPopupLoc); } #pragma mark - // --------------------------------------------------------------------------- // ¥ SetValue // --------------------------------------------------------------------------- // This is overridden to adjust the mark on the previous/new menu items and // to ensure that the value is always broadcast (even if it hasn't really // changed) and to call a "hook" for derived classes to perform some action // on value changes. // // Note that this implementation should be considered "final" (i.e. no further // overrides allowed) in order to preserve the semantics of this class. // // Exceptions thrown: CValueRangeException, CAttemptToSetDisabledValueException void CPatternButtonPopup::SetValue(Int32 inValue) { ThrowIfNil_(GetMenu()); ThrowIfNil_(GetMenu()->GetMacMenuH()); if (inValue < 0 || inValue > ::CountMItems(GetMenu()->GetMacMenuH())) { throw CValueRangeException(); } if (!GetMenu()->ItemIsEnabled(inValue)) { throw CAttemptToSetDisabledValueException(); } // If mMarkCurrentItem is false, it is assumed that we want to // be able to select the same command several times in a row. // By hacking mValue to 0, we make sure that the menu command // will always be broadcast. if (!mMarkCurrentItem) { mValue = 0; } if (mValue != inValue) { // Setup the menu item SetupCurrentMenuItem(GetMenu()->GetMacMenuH(), inValue); // Set the new value. If the HandleNewValue hook indicates that // the value was handled, then we make sure that the call to // the base class SetValue does not broadcast the new value. if (HandleNewValue(inValue)) { StSetBroadcasting setBroadcasting(this, false); super::SetValue(inValue); } else { super::SetValue(inValue); } } } // --------------------------------------------------------------------------- // ¥ SetPopupMinMaxValues // --------------------------------------------------------------------------- // Setup the minimum and maximum values for the control based on the // current menu state. // // NOTE: It is important for clients to call this method after inserting/removing // menu items dynamically. void CPatternButtonPopup::SetPopupMinMaxValues() { if (GetMenu() && GetMenu()->GetMacMenuH()) { mMinValue = 0; mMaxValue = ::CountMItems(GetMenu()->GetMacMenuH()); } else { mMaxValue = mMinValue = 0; } } #pragma mark - // --------------------------------------------------------------------------- // ¥ OKToSendCommand // --------------------------------------------------------------------------- Boolean CPatternButtonPopup::OKToSendCommand( Int32 inCommand) { LCommander* theTarget = LCommander::GetTarget(); Boolean enabled = false; Boolean usesMark = false; Str255 outName; Char16 outMark; if (inCommand && theTarget) { theTarget->ProcessCommandStatus(inCommand, enabled, usesMark, outMark, outName); } return enabled; } // --------------------------------------------------------------------------- // ¥ BroadcastValueMessage // --------------------------------------------------------------------------- // Broadcast the menu command if there is one, otherwise broadcast the value void CPatternButtonPopup::BroadcastValueMessage() { ThrowIfNil_(GetMenu()); ThrowIfNil_(GetMenu()->GetMacMenuH()); if (mValueMessage != msg_Nothing) { CommandT theCommand = GetMenu()->CommandFromIndex(GetValue()); if (theCommand != cmd_Nothing) { if (OKToSendCommand(theCommand)) { BroadcastMessage(theCommand, nil); } } else { Int32 value = mValue; BroadcastMessage(mValueMessage, &value); } } } #pragma mark - // --------------------------------------------------------------------------- // ¥ HandleQuickClick // --------------------------------------------------------------------------- // Hook for handling quick clicks. void CPatternButtonPopup::HandleQuickClick() { if (mQuickClickValueOrCommand) { if (mQuickClickIsCommandBased) { if (OKToSendCommand(mQuickClickValueOrCommand)) { BroadcastMessage(mQuickClickValueOrCommand, nil); } } else { try { SetValue(mQuickClickValueOrCommand); } catch (const CValueRangeException&) { } catch (const CAttemptToSetDisabledValueException&) { } } } } // --------------------------------------------------------------------------- // ¥ HandleNewValue // --------------------------------------------------------------------------- // Hook for handling value changes. Called by SetValue. // // Some CPatternButtonPopups may override this to perform an action rather // than using broadcaster/listener. Returns true if HandleNewValue handled the new // value (this will cause the new value to not be broadcast). // // Note that the setting of the new value is done by CPatternButtonPopup::SetValue. // Therefore, GetValue() will still return the old value here, so the old value is // still available in this method. Boolean CPatternButtonPopup::HandleNewValue(Int32 inNewValue) { #pragma unused(inNewValue) return false; } #pragma mark - // --------------------------------------------------------------------------- // ¥ HandleEnablingPolicy // --------------------------------------------------------------------------- // This is our required implementation of the MPaneEnablerPolicy interface. // // The enabling policy is as follows: // // - If all of the menu items associated with the menu items are disabled // then disable the button. Note that a menu item without a command // is automatically enabled and a menu item with a command is enabled // only if the command associated with it is enabled. // - If there is (no menu or no menu items in the menu) and // mQuickClickIsCommandBased is true and it is *not OK* to send the // mQuickClickValueOrCommand command then disable the button. // - Otherwise enable the buton. void CPatternButtonPopup::HandleEnablingPolicy() { LCommander* theTarget = LCommander::GetTarget(); MenuHandle macMenuH = nil; MessageT buttonCommand = GetValueMessage(); Boolean enabled = false; Boolean usesMark = false; Str255 outName; Char16 outMark; if (!CTargetedUpdateMenuRegistry::UseRegistryToUpdateMenus() || CTargetedUpdateMenuRegistry::CommandInRegistry(buttonCommand)) { if (!IsActive() || !IsVisible()) return; if (!theTarget) return; if (GetMenu() && GetMenu()->GetMacMenuH() && ::CountMItems(GetMenu()->GetMacMenuH())) { macMenuH = GetMenu()->GetMacMenuH(); } if (macMenuH) { // We have a menu CommandT theCommand; Boolean allDisabled = true; Int16 menuItem = 0; while (GetMenu()->FindNextCommand(menuItem, theCommand)) { if (theCommand) { outName[0] = 0; // Call ProcessCommandStatus() to determine whether to enable the menu items. // If they are all disabled then disable the button itself. theTarget->ProcessCommandStatus(theCommand, enabled, usesMark, outMark, outName); if (outName[0] != 0) { ::SetMenuItemText(macMenuH, menuItem, outName); } if (enabled) { allDisabled = false; ::EnableItem(macMenuH, menuItem); } else { ::DisableItem(macMenuH, menuItem); } if (usesMark) { if ( outMark && mMarkCharacter ) outMark = mMarkCharacter; ::SetItemMark(macMenuH, menuItem, outMark); } } else { allDisabled = false; ::EnableItem(macMenuH, menuItem); } } if (menuItem == 0) { // The menu does not have any commands, so we check the command attached to // the button itself. theTarget->ProcessCommandStatus(buttonCommand, enabled, usesMark, outMark, outName); allDisabled = (!enabled); } if (allDisabled) { Disable(); } else { Enable(); } } else { // We don't have a menu if (mQuickClickIsCommandBased && OKToSendCommand(mQuickClickValueOrCommand)) { Enable(); } else { Disable(); } } } }