/* ============================================================================== This file is part of the JUCE library - "Jules' Utility Class Extensions" Copyright 2004-7 by Raw Material Software ltd. ------------------------------------------------------------------------------ JUCE can be redistributed and/or modified under the terms of the GNU General Public License, as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with JUCE; if not, visit www.gnu.org/licenses or write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ------------------------------------------------------------------------------ If you'd like to release a closed-source product which uses JUCE, commercial licenses are also available: visit www.rawmaterialsoftware.com/juce for more information. ============================================================================== */ #include "../../../../juce_core/basics/juce_StandardHeader.h" BEGIN_JUCE_NAMESPACE #include "juce_PopupMenu.h" #include "../lookandfeel/juce_LookAndFeel.h" #include "../juce_ComponentDeletionWatcher.h" #include "../juce_Desktop.h" #include "../../graphics/imaging/juce_Image.h" #include "../keyboard/juce_KeyPressMappingSet.h" #include "../../../events/juce_Timer.h" #include "../../../../juce_core/threads/juce_Process.h" #include "../../../../juce_core/basics/juce_Time.h" static VoidArray activeMenuWindows; //============================================================================== class MenuItemInfo { public: //============================================================================== const int itemId; String text; const Colour textColour; const bool active, isSeparator, isTicked, usesColour; Image* image; PopupMenuCustomComponent* const customComp; PopupMenu* subMenu; ApplicationCommandManager* const commandManager; //============================================================================== MenuItemInfo() throw() : itemId (0), active (true), isSeparator (true), isTicked (false), usesColour (false), image (0), customComp (0), subMenu (0), commandManager (0) { } MenuItemInfo (const int itemId_, const String& text_, const bool active_, const bool isTicked_, const Image* im, const Colour& textColour_, const bool usesColour_, PopupMenuCustomComponent* const customComp_, const PopupMenu* const subMenu_, ApplicationCommandManager* const commandManager_) throw() : itemId (itemId_), text (text_), textColour (textColour_), active (active_), isSeparator (false), isTicked (isTicked_), usesColour (usesColour_), image (0), customComp (customComp_), commandManager (commandManager_) { subMenu = (subMenu_ != 0) ? new PopupMenu (*subMenu_) : 0; if (customComp != 0) customComp->refCount_++; if (im != 0) image = im->createCopy(); if (commandManager_ != 0 && itemId_ != 0) { String shortcutKey; Array keyPresses (commandManager_->getKeyMappings() ->getKeyPressesAssignedToCommand (itemId_)); for (int i = 0; i < keyPresses.size(); ++i) { const String key (keyPresses.getReference(i).getTextDescription()); if (shortcutKey.isNotEmpty()) shortcutKey << ", "; if (key.length() == 1) shortcutKey << "shortcut: '" << key << '\''; else shortcutKey << key; } shortcutKey = shortcutKey.trim(); if (shortcutKey.isNotEmpty()) text << "" << shortcutKey; } } MenuItemInfo (const MenuItemInfo& other) throw() : itemId (other.itemId), text (other.text), textColour (other.textColour), active (other.active), isSeparator (other.isSeparator), isTicked (other.isTicked), usesColour (other.usesColour), customComp (other.customComp), commandManager (other.commandManager) { if (other.subMenu != 0) subMenu = new PopupMenu (*(other.subMenu)); else subMenu = 0; if (other.image != 0) image = other.image->createCopy(); else image = 0; if (customComp != 0) customComp->refCount_++; } ~MenuItemInfo() throw() { delete subMenu; delete image; if (customComp != 0 && --(customComp->refCount_) == 0) delete customComp; } bool canBeTriggered() const throw() { return active && ! (isSeparator || (subMenu != 0)); } bool hasActiveSubMenu() const throw() { return active && (subMenu != 0); } juce_UseDebuggingNewOperator private: const MenuItemInfo& operator= (const MenuItemInfo&); }; //============================================================================== class MenuItemComponent : public Component { bool isHighlighted; public: MenuItemInfo itemInfo; //============================================================================== MenuItemComponent (const MenuItemInfo& itemInfo_) : isHighlighted (false), itemInfo (itemInfo_) { if (itemInfo.customComp != 0) addAndMakeVisible (itemInfo.customComp); } ~MenuItemComponent() { if (itemInfo.customComp != 0) removeChildComponent (itemInfo.customComp); } void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight) { if (itemInfo.customComp != 0) { itemInfo.customComp->getIdealSize (idealWidth, idealHeight); } else { getLookAndFeel().getIdealPopupMenuItemSize (itemInfo.text, itemInfo.isSeparator, standardItemHeight, idealWidth, idealHeight); } } void paint (Graphics& g) { if (itemInfo.customComp == 0) { String mainText (itemInfo.text); String endText; const int endIndex = mainText.indexOf (T("")); if (endIndex >= 0) { endText = mainText.substring (endIndex + 5).trim(); mainText = mainText.substring (0, endIndex); } getLookAndFeel() .drawPopupMenuItem (g, getWidth(), getHeight(), itemInfo.isSeparator, itemInfo.active, isHighlighted, itemInfo.isTicked, itemInfo.subMenu != 0, mainText, endText, itemInfo.image, itemInfo.usesColour ? &(itemInfo.textColour) : 0); } } void resized() { if (getNumChildComponents() > 0) getChildComponent(0)->setBounds (2, 0, getWidth() - 4, getHeight()); } void setHighlighted (bool shouldBeHighlighted) { shouldBeHighlighted = shouldBeHighlighted && itemInfo.active; if (isHighlighted != shouldBeHighlighted) { isHighlighted = shouldBeHighlighted; if (itemInfo.customComp != 0) { itemInfo.customComp->isHighlighted = shouldBeHighlighted; itemInfo.customComp->repaint(); } repaint(); } } private: MenuItemComponent (const MenuItemComponent&); const MenuItemComponent& operator= (const MenuItemComponent&); }; //============================================================================== static const int scrollZone = 24; static const int borderSize = 2; static const int timerInterval = 50; static const int dismissCommandId = 0x6287345f; static bool wasHiddenBecauseOfAppChange = false; //============================================================================== class PopupMenuWindow : public Component, private Timer { public: //============================================================================== PopupMenuWindow() throw() : Component (T("menu")), owner (0), currentChild (0), activeSubMenu (0), menuBarComponent (0), managerOfChosenCommand (0), componentAttachedTo (0), attachedCompWatcher (0), lastMouseX (0), lastMouseY (0), minimumWidth (0), maximumNumColumns (7), standardItemHeight (0), isOver (false), hasBeenOver (false), isDown (false), needsToScroll (false), hideOnExit (false), disableMouseMoves (false), hasAnyJuceCompHadFocus (false), numColumns (0), contentHeight (0), childYOffset (0), timeEnteredCurrentChildComp (0), scrollAcceleration (1.0) { menuCreationTime = lastFocused = lastScroll = Time::getMillisecondCounter(); setWantsKeyboardFocus (true); setOpaque (true); setAlwaysOnTop (true); Desktop::getInstance().addGlobalMouseListener (this); activeMenuWindows.add (this); } ~PopupMenuWindow() { activeMenuWindows.removeValue (this); Desktop::getInstance().removeGlobalMouseListener (this); jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent()); delete activeSubMenu; deleteAllChildren(); delete attachedCompWatcher; } //============================================================================== static PopupMenuWindow* create (const PopupMenu& menu, const bool dismissOnMouseUp, PopupMenuWindow* const owner_, const int minX, const int maxX, const int minY, const int maxY, const int minimumWidth, const int maximumNumColumns, const int standardItemHeight, const bool alignToRectangle, const int itemIdThatMustBeVisible, Component* const menuBarComponent, ApplicationCommandManager** managerOfChosenCommand, Component* const componentAttachedTo) throw() { if (menu.items.size() > 0) { int totalItems = 0; PopupMenuWindow* const mw = new PopupMenuWindow(); mw->setLookAndFeel (menu.lookAndFeel); mw->setWantsKeyboardFocus (false); mw->minimumWidth = minimumWidth; mw->maximumNumColumns = maximumNumColumns; mw->standardItemHeight = standardItemHeight; mw->dismissOnMouseUp = dismissOnMouseUp; for (int i = 0; i < menu.items.size(); ++i) { MenuItemInfo* const item = (MenuItemInfo*) menu.items.getUnchecked(i); mw->addItem (*item); ++totalItems; } if (totalItems == 0) { delete mw; } else { mw->owner = owner_; mw->menuBarComponent = menuBarComponent; mw->managerOfChosenCommand = managerOfChosenCommand; mw->componentAttachedTo = componentAttachedTo; delete mw->attachedCompWatcher; mw->attachedCompWatcher = componentAttachedTo != 0 ? new ComponentDeletionWatcher (componentAttachedTo) : 0; mw->calculateWindowPos (minX, maxX, minY, maxY, alignToRectangle); mw->setTopLeftPosition (mw->windowPos.getX(), mw->windowPos.getY()); mw->updateYPositions(); if (itemIdThatMustBeVisible != 0) { const int y = minY - mw->windowPos.getY(); mw->ensureItemIsVisible (itemIdThatMustBeVisible, (((unsigned int) y) < (unsigned int) mw->windowPos.getHeight()) ? y : -1); } mw->resizeToBestWindowPos(); mw->addToDesktop (ComponentPeer::windowIsTemporary | mw->getLookAndFeel().getMenuWindowFlags()); return mw; } } return 0; } //============================================================================== void paint (Graphics& g) { getLookAndFeel().drawPopupMenuBackground (g, getWidth(), getHeight()); } void paintOverChildren (Graphics& g) { if (isScrolling()) { LookAndFeel& lf = getLookAndFeel(); if (isScrollZoneActive (false)) lf.drawPopupMenuUpDownArrow (g, getWidth(), scrollZone, true); if (isScrollZoneActive (true)) { g.setOrigin (0, getHeight() - scrollZone); lf.drawPopupMenuUpDownArrow (g, getWidth(), scrollZone, false); } } } bool isScrollZoneActive (bool bottomOne) const { return isScrolling() && (bottomOne ? childYOffset < contentHeight - windowPos.getHeight() : childYOffset > 0); } //============================================================================== void addItem (const MenuItemInfo& item) throw() { MenuItemComponent* const mic = new MenuItemComponent (item); addAndMakeVisible (mic); int itemW = 80; int itemH = 16; mic->getIdealSize (itemW, itemH, standardItemHeight); mic->setSize (itemW, jlimit (10, 600, itemH)); mic->addMouseListener (this, false); } //============================================================================== // hide this and all sub-comps void hide (const MenuItemInfo* const item) throw() { if (isVisible()) { jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent()); deleteAndZero (activeSubMenu); currentChild = 0; exitModalState (item != 0 ? item->itemId : 0); setVisible (false); if (item != 0 && item->commandManager != 0 && item->itemId != 0) { *managerOfChosenCommand = item->commandManager; } } } void dismissMenu (const MenuItemInfo* const item) throw() { if (owner != 0) { owner->dismissMenu (item); } else { if (item != 0) { // need a copy of this on the stack as the one passed in will get deleted during this call const MenuItemInfo mi (*item); hide (&mi); } else { hide (0); } } } //============================================================================== void mouseMove (const MouseEvent&) { timerCallback(); } void mouseDown (const MouseEvent&) { timerCallback(); } void mouseDrag (const MouseEvent&) { timerCallback(); } void mouseUp (const MouseEvent&) { timerCallback(); } void mouseWheelMove (const MouseEvent&, float /*amountX*/, float amountY) { alterChildYPos (roundFloatToInt (-10.0f * amountY * scrollZone)); lastMouseX = -1; } bool keyPressed (const KeyPress& key) { if (key.isKeyCode (KeyPress::downKey)) { selectNextItem (1); } else if (key.isKeyCode (KeyPress::upKey)) { selectNextItem (-1); } else if (key.isKeyCode (KeyPress::leftKey)) { PopupMenuWindow* parentWindow = owner; if (parentWindow != 0) { MenuItemComponent* currentChildOfParent = (parentWindow != 0) ? parentWindow->currentChild : 0; hide (0); if (parentWindow->isValidComponent()) parentWindow->setCurrentlyHighlightedChild (currentChildOfParent); disableTimerUntilMouseMoves(); } else if (menuBarComponent != 0) { menuBarComponent->keyPressed (key); } } else if (key.isKeyCode (KeyPress::rightKey)) { disableTimerUntilMouseMoves(); if (showSubMenuFor (currentChild)) { jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent()); if (activeSubMenu != 0 && activeSubMenu->isVisible()) activeSubMenu->selectNextItem (1); } else if (menuBarComponent != 0) { menuBarComponent->keyPressed (key); } } else if (key.isKeyCode (KeyPress::returnKey)) { triggerCurrentlyHighlightedItem(); } else if (key.isKeyCode (KeyPress::escapeKey)) { dismissMenu (0); } else { return false; } return true; } void inputAttemptWhenModal() { timerCallback(); if (! isOverAnyMenu()) { if (componentAttachedTo != 0 && ! attachedCompWatcher->hasBeenDeleted()) { // we want to dismiss the menu, but if we do it synchronously, then // the mouse-click will be allowed to pass through. That's good, except // when the user clicks on the button that orginally popped the menu up, // as they'll expect the menu to go away, and in fact it'll just // come back. So only dismiss synchronously if they're not on the original // comp that we're attached to. int mx, my; componentAttachedTo->getMouseXYRelative (mx, my); if (componentAttachedTo->reallyContains (mx, my, true)) { postCommandMessage (dismissCommandId); // dismiss asynchrounously return; } } dismissMenu (0); } } void handleCommandMessage (int commandId) { Component::handleCommandMessage (commandId); if (commandId == dismissCommandId) dismissMenu (0); } //============================================================================== void timerCallback() { if (! isVisible()) return; if (attachedCompWatcher != 0 && attachedCompWatcher->hasBeenDeleted()) { dismissMenu (0); return; } PopupMenuWindow* currentlyModalWindow = dynamic_cast (Component::getCurrentlyModalComponent()); if (currentlyModalWindow != 0 && ! treeContains (currentlyModalWindow)) return; startTimer (timerInterval); // do this in case it was called from a mouse // move rather than a real timer callback int mx, my; Desktop::getMousePosition (mx, my); int x = mx, y = my; globalPositionToRelative (x, y); const uint32 now = Time::getMillisecondCounter(); if (now > timeEnteredCurrentChildComp + 100 && reallyContains (x, y, true) && currentChild->isValidComponent() && (! disableMouseMoves) && ! (activeSubMenu != 0 && activeSubMenu->isVisible())) { showSubMenuFor (currentChild); } if (mx != lastMouseX || my != lastMouseY || now > lastMouseMoveTime + 350) { highlightItemUnderMouse (mx, my, x, y); } bool overScrollArea = false; if (isScrolling() && (isOver || (isDown && ((unsigned int) x) < (unsigned int) getWidth())) && ((isScrollZoneActive (false) && y < scrollZone) || (isScrollZoneActive (true) && y > getHeight() - scrollZone))) { if (now > lastScroll + 20) { scrollAcceleration = jmin (4.0, scrollAcceleration * 1.04); int amount = 0; for (int i = 0; i < getNumChildComponents() && amount == 0; ++i) amount = ((int) scrollAcceleration) * getChildComponent (i)->getHeight(); alterChildYPos (y < scrollZone ? -amount : amount); lastScroll = now; } overScrollArea = true; lastMouseX = -1; // trigger a mouse-move } else { scrollAcceleration = 1.0; } const bool wasDown = isDown; bool isOverAny = isOverAnyMenu(); if (hideOnExit && hasBeenOver && (! isOverAny) && activeSubMenu != 0) { activeSubMenu->updateMouseOverStatus (mx, my); isOverAny = isOverAnyMenu(); } if (hideOnExit && hasBeenOver && ! isOverAny) { hide (0); } else { isDown = hasBeenOver && (ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown() || ModifierKeys::getCurrentModifiersRealtime().isAnyMouseButtonDown()); bool anyFocused = Process::isForegroundProcess(); if (anyFocused && Component::getCurrentlyFocusedComponent() == 0) { // because no component at all may have focus, our test here will // only be triggered when something has focus and then loses it. anyFocused = ! hasAnyJuceCompHadFocus; for (int i = ComponentPeer::getNumPeers(); --i >= 0;) { if (ComponentPeer::getPeer (i)->isFocused()) { anyFocused = true; hasAnyJuceCompHadFocus = true; break; } } } if (! anyFocused) { if (now > lastFocused + 10) { wasHiddenBecauseOfAppChange = true; dismissMenu (0); return; // may have been deleted by the previous call.. } } else if (wasDown && now > menuCreationTime + 250 && ! (isDown || overScrollArea)) { isOver = reallyContains (x, y, true); if (isOver) { triggerCurrentlyHighlightedItem(); } else if ((hasBeenOver || ! dismissOnMouseUp) && ! isOverAny) { dismissMenu (0); } return; // may have been deleted by the previous calls.. } else { lastFocused = now; } } } //============================================================================== juce_UseDebuggingNewOperator private: PopupMenuWindow* owner; MenuItemComponent* currentChild; PopupMenuWindow* activeSubMenu; Component* menuBarComponent; ApplicationCommandManager** managerOfChosenCommand; Component* componentAttachedTo; ComponentDeletionWatcher* attachedCompWatcher; Rectangle windowPos; int lastMouseX, lastMouseY; int minimumWidth, maximumNumColumns, standardItemHeight; bool isOver, hasBeenOver, isDown, needsToScroll; bool dismissOnMouseUp, hideOnExit, disableMouseMoves, hasAnyJuceCompHadFocus; int numColumns, contentHeight, childYOffset; Array columnWidths; uint32 menuCreationTime, lastFocused, lastScroll, lastMouseMoveTime, timeEnteredCurrentChildComp; double scrollAcceleration; //============================================================================== bool overlaps (const Rectangle& r) const throw() { return r.intersects (getBounds()) || (owner != 0 && owner->overlaps (r)); } bool isOverAnyMenu() const throw() { return (owner != 0) ? owner->isOverAnyMenu() : isOverChildren(); } bool isOverChildren() const throw() { jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent()); return isVisible() && (isOver || (activeSubMenu != 0 && activeSubMenu->isOverChildren())); } void updateMouseOverStatus (const int mx, const int my) throw() { int rx = mx, ry = my; globalPositionToRelative (rx, ry); isOver = reallyContains (rx, ry, true); if (activeSubMenu != 0) activeSubMenu->updateMouseOverStatus (mx, my); } bool treeContains (const PopupMenuWindow* const window) const throw() { const PopupMenuWindow* mw = this; while (mw->owner != 0) mw = mw->owner; while (mw != 0) { if (mw == window) return true; mw = mw->activeSubMenu; } return false; } //============================================================================== void calculateWindowPos (const int minX, const int maxX, const int minY, const int maxY, const bool alignToRectangle) { const Rectangle mon (Desktop::getInstance() .getMonitorAreaContaining ((minX + maxX) / 2, (minY + maxY) / 2, true)); int x, y, widthToUse, heightToUse; layoutMenuItems (mon.getWidth() - 24, widthToUse, heightToUse); if (alignToRectangle) { x = minX; const int spaceUnder = mon.getHeight() - maxY; const int spaceOver = minY - mon.getY(); if (heightToUse < spaceUnder - 30 || spaceUnder >= spaceOver) y = maxY; else y = minY - heightToUse; } else { bool tendTowardsRight = (minX + maxX) / 2 < mon.getCentreX(); if (owner != 0) { if (owner->owner != 0) { const bool ownerGoingRight = (owner->getX() + owner->getWidth() / 2 > owner->owner->getX() + owner->owner->getWidth() / 2); if (ownerGoingRight && maxX + widthToUse < mon.getRight() - 4) tendTowardsRight = true; else if ((! ownerGoingRight) && minX > widthToUse + 4) tendTowardsRight = false; } else if (maxX + widthToUse < mon.getRight() - 32) { tendTowardsRight = true; } } const int biggestSpace = jmax (mon.getRight() - maxX, minX - mon.getX()) - 32; if (biggestSpace < widthToUse) { layoutMenuItems (biggestSpace + (maxX - minX) / 3, widthToUse, heightToUse); if (numColumns > 1) layoutMenuItems (biggestSpace - 4, widthToUse, heightToUse); tendTowardsRight = (mon.getRight() - maxX) >= (minX - mon.getX()); } if (tendTowardsRight) x = jmin (mon.getRight() - widthToUse - 4, maxX); else x = jmax (mon.getX() + 4, minX - widthToUse); y = minY; if ((minY + maxY) / 2 > mon.getCentreY()) y = jmax (mon.getY(), maxY - heightToUse); } x = jlimit (mon.getX() + 1, mon.getRight() - (widthToUse + 6), x); y = jlimit (mon.getY() + 1, mon.getBottom() - (heightToUse + 6), y); windowPos.setBounds (x, y, widthToUse, heightToUse); // sets this flag if it's big enough to obscure any of its parent menus hideOnExit = (owner != 0) && owner->windowPos.intersects (windowPos.expanded (-4, -4)); } void layoutMenuItems (const int maxMenuW, int& width, int& height) { numColumns = 0; contentHeight = 0; const int maxMenuH = getParentHeight() - 24; int totalW; do { ++numColumns; totalW = workOutBestSize (numColumns, maxMenuW); if (totalW > maxMenuW) { numColumns = jmax (1, numColumns - 1); totalW = workOutBestSize (numColumns, maxMenuW); // to update col widths break; } else if (totalW > maxMenuW / 2 || contentHeight < maxMenuH) { break; } } while (numColumns < maximumNumColumns); const int actualH = jmin (contentHeight, maxMenuH); needsToScroll = contentHeight > actualH; width = updateYPositions(); height = actualH + borderSize * 2; } int workOutBestSize (const int numColumns, const int maxMenuW) { int totalW = 0; contentHeight = 0; int childNum = 0; for (int col = 0; col < numColumns; ++col) { int i, colW = 50, colH = 0; const int numChildren = jmin (getNumChildComponents() - childNum, (getNumChildComponents() + numColumns - 1) / numColumns); for (i = numChildren; --i >= 0;) { colW = jmax (colW, getChildComponent (childNum + i)->getWidth()); colH += getChildComponent (childNum + i)->getHeight(); } colW = jmin (maxMenuW / jmax (1, numColumns - 2), colW + borderSize * 2); columnWidths.set (col, colW); totalW += colW; contentHeight = jmax (contentHeight, colH); childNum += numChildren; } if (totalW < minimumWidth) { totalW = minimumWidth; for (int col = 0; col < numColumns; ++col) columnWidths.set (0, totalW / numColumns); } return totalW; } void ensureItemIsVisible (const int itemId, int wantedY) { jassert (itemId != 0) for (int i = getNumChildComponents(); --i >= 0;) { MenuItemComponent* const m = (MenuItemComponent*) getChildComponent (i); if (m != 0 && m->itemInfo.itemId == itemId && windowPos.getHeight() > scrollZone * 4) { const int currentY = m->getY(); if (wantedY > 0 || currentY < 0 || m->getBottom() > windowPos.getHeight()) { if (wantedY < 0) wantedY = jlimit (scrollZone, jmax (scrollZone, windowPos.getHeight() - (scrollZone + m->getHeight())), currentY); const Rectangle mon (Desktop::getInstance() .getMonitorAreaContaining (windowPos.getX(), windowPos.getY(), true)); int deltaY = wantedY - currentY; const int newY = jlimit (mon.getY(), mon.getBottom() - windowPos.getHeight(), windowPos.getY() + deltaY); deltaY -= newY - windowPos.getY(); childYOffset -= deltaY; windowPos.setPosition (windowPos.getX(), newY); updateYPositions(); } break; } } } void resizeToBestWindowPos() { Rectangle r (windowPos); if (childYOffset < 0) { r.setBounds (r.getX(), r.getY() - childYOffset, r.getWidth(), r.getHeight() + childYOffset); } else if (childYOffset > 0) { const int spaceAtBottom = r.getHeight() - (contentHeight - childYOffset); if (spaceAtBottom > 0) r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom); } setBounds (r); updateYPositions(); } void alterChildYPos (const int delta) { if (isScrolling()) { childYOffset += delta; if (delta < 0) { childYOffset = jmax (childYOffset, 0); } else if (delta > 0) { childYOffset = jmin (childYOffset, contentHeight - windowPos.getHeight() + borderSize); } updateYPositions(); } else { childYOffset = 0; } resizeToBestWindowPos(); repaint(); } int updateYPositions() { int x = 0; int childNum = 0; for (int col = 0; col < numColumns; ++col) { const int numChildren = jmin (getNumChildComponents() - childNum, (getNumChildComponents() + numColumns - 1) / numColumns); const int colW = columnWidths [col]; int y = borderSize - (childYOffset + (getY() - windowPos.getY())); for (int i = 0; i < numChildren; ++i) { Component* const c = getChildComponent (childNum + i); c->setBounds (x, y, colW, c->getHeight()); y += c->getHeight(); } x += colW; childNum += numChildren; } return x; } bool isScrolling() const throw() { return childYOffset != 0 || needsToScroll; } void setCurrentlyHighlightedChild (MenuItemComponent* const child) throw() { if (currentChild->isValidComponent()) currentChild->setHighlighted (false); currentChild = child; if (currentChild != 0) { currentChild->setHighlighted (true); timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter(); } } bool showSubMenuFor (MenuItemComponent* const childComp) { jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent()); deleteAndZero (activeSubMenu); if (childComp->isValidComponent() && childComp->itemInfo.hasActiveSubMenu()) { int left = 0, top = 0; childComp->relativePositionToGlobal (left, top); int right = childComp->getWidth(), bottom = childComp->getHeight(); childComp->relativePositionToGlobal (right, bottom); activeSubMenu = PopupMenuWindow::create (*(childComp->itemInfo.subMenu), dismissOnMouseUp, this, left, right, top, bottom, 0, maximumNumColumns, standardItemHeight, false, 0, menuBarComponent, managerOfChosenCommand, componentAttachedTo); if (activeSubMenu != 0) { activeSubMenu->setVisible (true); activeSubMenu->enterModalState (false); activeSubMenu->toFront (false); return true; } } return false; } void highlightItemUnderMouse (const int mx, const int my, const int x, const int y) { isOver = reallyContains (x, y, true); if (isOver) hasBeenOver = true; if (abs (lastMouseX - mx) > 2 || abs (lastMouseY - my) > 2) { lastMouseMoveTime = Time::getApproximateMillisecondCounter(); if (disableMouseMoves && isOver) disableMouseMoves = false; } if (disableMouseMoves) return; bool isMovingTowardsMenu = false; jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent()) if (isOver && (activeSubMenu != 0) && (mx != lastMouseX || my != lastMouseY)) { // try to intelligently guess whether the user is moving the mouse towards a currently-open // submenu. To do this, look at whether the mouse stays inside a triangular region that // extends from the last mouse pos to the submenu's rectangle.. float subX = (float) activeSubMenu->getScreenX(); if (activeSubMenu->getX() > getX()) { lastMouseX -= 2; // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels } else { lastMouseX += 2; subX += activeSubMenu->getWidth(); } Path areaTowardsSubMenu; areaTowardsSubMenu.addTriangle ((float) lastMouseX, (float) lastMouseY, subX, (float) activeSubMenu->getScreenY(), subX, (float) (activeSubMenu->getScreenY() + activeSubMenu->getHeight())); isMovingTowardsMenu = areaTowardsSubMenu.contains ((float) mx, (float) my); } lastMouseX = mx; lastMouseY = my; if (! isMovingTowardsMenu) { Component* c = getComponentAt (x, y); if (c == this) c = 0; MenuItemComponent* mic = dynamic_cast (c); if (mic == 0 && c != 0) mic = c->findParentComponentOfClass ((MenuItemComponent*) 0); if (mic != currentChild && (isOver || (activeSubMenu == 0) || ! activeSubMenu->isVisible())) { if (isOver && (c != 0) && (activeSubMenu != 0)) { activeSubMenu->hide (0); } if (! isOver) mic = 0; setCurrentlyHighlightedChild (mic); } } } void triggerCurrentlyHighlightedItem() { if (currentChild->isValidComponent() && currentChild->itemInfo.canBeTriggered() && (currentChild->itemInfo.customComp == 0 || currentChild->itemInfo.customComp->isTriggeredAutomatically)) { dismissMenu (¤tChild->itemInfo); } } void selectNextItem (const int delta) { disableTimerUntilMouseMoves(); MenuItemComponent* mic = 0; bool wasLastOne = (currentChild == 0); const int numItems = getNumChildComponents(); for (int i = 0; i < numItems + 1; ++i) { int index = (delta > 0) ? i : (numItems - 1 - i); index = (index + numItems) % numItems; mic = dynamic_cast (getChildComponent (index)); if (mic != 0 && (mic->itemInfo.canBeTriggered() || mic->itemInfo.hasActiveSubMenu()) && wasLastOne) break; if (mic == currentChild) wasLastOne = true; } setCurrentlyHighlightedChild (mic); } void disableTimerUntilMouseMoves() throw() { disableMouseMoves = true; if (owner != 0) owner->disableTimerUntilMouseMoves(); } PopupMenuWindow (const PopupMenuWindow&); const PopupMenuWindow& operator= (const PopupMenuWindow&); }; //============================================================================== PopupMenu::PopupMenu() throw() : items (8), lookAndFeel (0), separatorPending (false) { } PopupMenu::PopupMenu (const PopupMenu& other) throw() : items (8), lookAndFeel (other.lookAndFeel), separatorPending (false) { items.ensureStorageAllocated (other.items.size()); for (int i = 0; i < other.items.size(); ++i) items.add (new MenuItemInfo (*(const MenuItemInfo*) other.items.getUnchecked(i))); } const PopupMenu& PopupMenu::operator= (const PopupMenu& other) throw() { if (this != &other) { lookAndFeel = other.lookAndFeel; clear(); items.ensureStorageAllocated (other.items.size()); for (int i = 0; i < other.items.size(); ++i) items.add (new MenuItemInfo (*(const MenuItemInfo*) other.items.getUnchecked(i))); } return *this; } PopupMenu::~PopupMenu() throw() { clear(); } void PopupMenu::clear() throw() { for (int i = items.size(); --i >= 0;) { MenuItemInfo* const mi = (MenuItemInfo*) items.getUnchecked(i); delete mi; } items.clear(); separatorPending = false; } void PopupMenu::addSeparatorIfPending() { if (separatorPending) { separatorPending = false; if (items.size() > 0) items.add (new MenuItemInfo()); } } void PopupMenu::addItem (const int itemResultId, const String& itemText, const bool isActive, const bool isTicked, const Image* const iconToUse) throw() { jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user // didn't pick anything, so you shouldn't use it as the id // for an item.. addSeparatorIfPending(); items.add (new MenuItemInfo (itemResultId, itemText, isActive, isTicked, iconToUse, Colours::black, false, 0, 0, 0)); } void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager, const int commandID, const String& displayName) throw() { jassert (commandManager != 0 && commandID != 0); const ApplicationCommandInfo* const registeredInfo = commandManager->getCommandForID (commandID); if (registeredInfo != 0) { ApplicationCommandInfo info (*registeredInfo); ApplicationCommandTarget* const target = commandManager->getTargetForCommand (commandID, info); addSeparatorIfPending(); items.add (new MenuItemInfo (commandID, displayName.isNotEmpty() ? displayName : info.shortName, target != 0 && (info.flags & ApplicationCommandInfo::isDisabled) == 0, (info.flags & ApplicationCommandInfo::isTicked) != 0, 0, Colours::black, false, 0, 0, commandManager)); } } void PopupMenu::addColouredItem (const int itemResultId, const String& itemText, const Colour& itemTextColour, const bool isActive, const bool isTicked, const Image* const iconToUse) throw() { jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user // didn't pick anything, so you shouldn't use it as the id // for an item.. addSeparatorIfPending(); items.add (new MenuItemInfo (itemResultId, itemText, isActive, isTicked, iconToUse, itemTextColour, true, 0, 0, 0)); } //============================================================================== void PopupMenu::addCustomItem (const int itemResultId, PopupMenuCustomComponent* const customComponent) throw() { jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user // didn't pick anything, so you shouldn't use it as the id // for an item.. addSeparatorIfPending(); items.add (new MenuItemInfo (itemResultId, String::empty, true, false, 0, Colours::black, false, customComponent, 0, 0)); } class NormalComponentWrapper : public PopupMenuCustomComponent { public: NormalComponentWrapper (Component* const comp, const int w, const int h, const bool triggerMenuItemAutomaticallyWhenClicked) : PopupMenuCustomComponent (triggerMenuItemAutomaticallyWhenClicked), width (w), height (h) { addAndMakeVisible (comp); } ~NormalComponentWrapper() {} void getIdealSize (int& idealWidth, int& idealHeight) { idealWidth = width; idealHeight = height; } void resized() { if (getChildComponent(0) != 0) getChildComponent(0)->setBounds (0, 0, getWidth(), getHeight()); } juce_UseDebuggingNewOperator private: const int width, height; NormalComponentWrapper (const NormalComponentWrapper&); const NormalComponentWrapper& operator= (const NormalComponentWrapper&); }; void PopupMenu::addCustomItem (const int itemResultId, Component* customComponent, int idealWidth, int idealHeight, const bool triggerMenuItemAutomaticallyWhenClicked) throw() { addCustomItem (itemResultId, new NormalComponentWrapper (customComponent, idealWidth, idealHeight, triggerMenuItemAutomaticallyWhenClicked)); } //============================================================================== void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, const bool isActive, Image* const iconToUse) throw() { addSeparatorIfPending(); items.add (new MenuItemInfo (0, subMenuName, isActive && (subMenu.getNumItems() > 0), false, iconToUse, Colours::black, false, 0, &subMenu, 0)); } void PopupMenu::addSeparator() throw() { separatorPending = true; } //============================================================================== class HeaderItemComponent : public PopupMenuCustomComponent { public: HeaderItemComponent (const String& name) : PopupMenuCustomComponent (false) { setName (name); } ~HeaderItemComponent() { } void paint (Graphics& g) { Font f (getLookAndFeel().getPopupMenuFont()); f.setBold (true); g.setFont (f); g.setColour (findColour (PopupMenu::headerTextColourId)); g.drawFittedText (getName(), 12, 0, getWidth() - 16, proportionOfHeight (0.8f), Justification::bottomLeft, 1); } void getIdealSize (int& idealWidth, int& idealHeight) { getLookAndFeel().getIdealPopupMenuItemSize (getName(), false, -1, idealWidth, idealHeight); idealHeight += idealHeight / 2; idealWidth += idealWidth / 4; } juce_UseDebuggingNewOperator }; void PopupMenu::addSectionHeader (const String& title) throw() { addCustomItem (0X4734a34f, new HeaderItemComponent (title)); } //============================================================================== Component* PopupMenu::createMenuComponent (const int x, const int y, const int w, const int h, const int itemIdThatMustBeVisible, const int minimumWidth, const int maximumNumColumns, const int standardItemHeight, const bool alignToRectangle, Component* menuBarComponent, ApplicationCommandManager** managerOfChosenCommand, Component* const componentAttachedTo) throw() { PopupMenuWindow* const pw = PopupMenuWindow::create (*this, ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown(), 0, x, x + w, y, y + h, minimumWidth, maximumNumColumns, standardItemHeight, alignToRectangle, itemIdThatMustBeVisible, menuBarComponent, managerOfChosenCommand, componentAttachedTo); if (pw != 0) pw->setVisible (true); return pw; } int PopupMenu::showMenu (const int x, const int y, const int w, const int h, const int itemIdThatMustBeVisible, const int minimumWidth, const int maximumNumColumns, const int standardItemHeight, const bool alignToRectangle, Component* const componentAttachedTo) throw() { Component* const prevFocused = Component::getCurrentlyFocusedComponent(); ComponentDeletionWatcher* deletionChecker1 = 0; if (prevFocused != 0) deletionChecker1 = new ComponentDeletionWatcher (prevFocused); Component* const prevTopLevel = (prevFocused != 0) ? prevFocused->getTopLevelComponent() : 0; ComponentDeletionWatcher* deletionChecker2 = 0; if (prevTopLevel != 0) deletionChecker2 = new ComponentDeletionWatcher (prevTopLevel); wasHiddenBecauseOfAppChange = false; int result = 0; ApplicationCommandManager* managerOfChosenCommand = 0; Component* const popupComp = createMenuComponent (x, y, w, h, itemIdThatMustBeVisible, minimumWidth, maximumNumColumns > 0 ? maximumNumColumns : 7, standardItemHeight, alignToRectangle, 0, &managerOfChosenCommand, componentAttachedTo); if (popupComp != 0) { popupComp->enterModalState (false); popupComp->toFront (false); // need to do this after making it modal, or it could // be stuck behind other comps that are already modal.. result = popupComp->runModalLoop(); delete popupComp; if (! wasHiddenBecauseOfAppChange) { if (deletionChecker2 != 0 && ! deletionChecker2->hasBeenDeleted()) prevTopLevel->toFront (true); if (deletionChecker1 != 0 && ! deletionChecker1->hasBeenDeleted()) prevFocused->grabKeyboardFocus(); } } delete deletionChecker1; delete deletionChecker2; if (managerOfChosenCommand != 0 && result != 0) { ApplicationCommandTarget::InvocationInfo info (result); info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu; managerOfChosenCommand->invoke (info, true); } return result; } int PopupMenu::show (const int itemIdThatMustBeVisible, const int minimumWidth, const int maximumNumColumns, const int standardItemHeight) { int x, y; Desktop::getMousePosition (x, y); return showAt (x, y, itemIdThatMustBeVisible, minimumWidth, maximumNumColumns, standardItemHeight); } int PopupMenu::showAt (const int screenX, const int screenY, const int itemIdThatMustBeVisible, const int minimumWidth, const int maximumNumColumns, const int standardItemHeight) { return showMenu (screenX, screenY, 1, 1, itemIdThatMustBeVisible, minimumWidth, maximumNumColumns, standardItemHeight, false, 0); } int PopupMenu::showAt (Component* componentToAttachTo, const int itemIdThatMustBeVisible, const int minimumWidth, const int maximumNumColumns, const int standardItemHeight) { if (componentToAttachTo != 0) { return showMenu (componentToAttachTo->getScreenX(), componentToAttachTo->getScreenY(), componentToAttachTo->getWidth(), componentToAttachTo->getHeight(), itemIdThatMustBeVisible, minimumWidth, maximumNumColumns, standardItemHeight, true, componentToAttachTo); } else { return show (itemIdThatMustBeVisible, minimumWidth, maximumNumColumns, standardItemHeight); } } void JUCE_CALLTYPE PopupMenu::dismissAllActiveMenus() throw() { for (int i = activeMenuWindows.size(); --i >= 0;) { PopupMenuWindow* const pmw = (PopupMenuWindow*) activeMenuWindows[i]; if (pmw != 0) pmw->dismissMenu (0); } } //============================================================================== int PopupMenu::getNumItems() const throw() { int num = 0; for (int i = items.size(); --i >= 0;) if (! ((MenuItemInfo*) items.getUnchecked(i))->isSeparator) ++num; return num; } bool PopupMenu::containsCommandItem (const int commandID) const throw() { for (int i = items.size(); --i >= 0;) { const MenuItemInfo* mi = (const MenuItemInfo*) items.getUnchecked (i); if ((mi->itemId == commandID && mi->commandManager != 0) || (mi->subMenu != 0 && mi->subMenu->containsCommandItem (commandID))) { return true; } } return false; } bool PopupMenu::containsAnyActiveItems() const throw() { for (int i = items.size(); --i >= 0;) { const MenuItemInfo* const mi = (const MenuItemInfo*) items.getUnchecked (i); if (mi->subMenu != 0) { if (mi->subMenu->containsAnyActiveItems()) return true; } else if (mi->active) { return true; } } return false; } void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel) throw() { lookAndFeel = newLookAndFeel; } //============================================================================== PopupMenuCustomComponent::PopupMenuCustomComponent (const bool isTriggeredAutomatically_) : refCount_ (0), isHighlighted (false), isTriggeredAutomatically (isTriggeredAutomatically_) { } PopupMenuCustomComponent::~PopupMenuCustomComponent() { jassert (refCount_ == 0); // should be deleted only by the menu component, as they keep a ref-count. } void PopupMenuCustomComponent::triggerMenuItem() { MenuItemComponent* const mic = dynamic_cast (getParentComponent()); if (mic != 0) { PopupMenuWindow* const pmw = dynamic_cast (mic->getParentComponent()); if (pmw != 0) { pmw->dismissMenu (&mic->itemInfo); } else { // something must have gone wrong with the component hierarchy if this happens.. jassertfalse } } else { // why isn't this component inside a menu? Not much point triggering the item if // there's no menu. jassertfalse } } //============================================================================== PopupMenu::MenuItemIterator::MenuItemIterator (const PopupMenu& menu_) throw() : subMenu (0), itemId (0), isSeparator (false), isTicked (false), isEnabled (false), isCustomComponent (false), isSectionHeader (false), customColour (0), customImage (0), menu (menu_), index (0) { } PopupMenu::MenuItemIterator::~MenuItemIterator() throw() { } bool PopupMenu::MenuItemIterator::next() throw() { if (index >= menu.items.size()) return false; const MenuItemInfo* const item = (const MenuItemInfo*) menu.items.getUnchecked (index); ++index; itemName = item->customComp != 0 ? item->customComp->getName() : item->text; subMenu = item->subMenu; itemId = item->itemId; isSeparator = item->isSeparator; isTicked = item->isTicked; isEnabled = item->active; isSectionHeader = dynamic_cast (item->customComp) != 0; isCustomComponent = (! isSectionHeader) && item->customComp != 0; customColour = item->usesColour ? &(item->textColour) : 0; customImage = item->image; commandManager = item->commandManager; return true; } END_JUCE_NAMESPACE