/*
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
This file is part of the JUCE library .
Copyright ( c ) 2015 - ROLI Ltd .
Permission is granted to use this software under the terms of either :
a ) the GPL v2 ( or any later version )
b ) the Affero GPL v3
Details of these licenses can be found at : www . gnu . org / licenses
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 .
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
To release a closed - source product which uses JUCE , commercial licenses are
available : visit www . juce . com for more information .
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
*/
//==============================================================================
namespace PopupMenuSettings
{
const int scrollZone = 24 ;
const int borderSize = 2 ;
const int timerInterval = 50 ;
const int dismissCommandId = 0x6287345f ;
const int sectionHeaderID = 0x4734a34f ;
static bool menuWasHiddenBecauseOfAppChange = false ;
}
class PopupMenu : : Item
{
public :
Item ( ) : itemID ( 0 ) , isActive ( true ) , isSeparator ( true ) , isTicked ( false ) ,
usesColour ( false ) , commandManager ( nullptr )
{ }
Item ( const int itemId ,
const String & name ,
const bool active ,
const bool ticked ,
Drawable * drawable ,
const Colour colour ,
const bool useColour ,
CustomComponent * const custom ,
const PopupMenu * const sub ,
ApplicationCommandManager * const manager )
: itemID ( itemId ) , text ( name ) , textColour ( colour ) ,
isActive ( active ) , isSeparator ( false ) , isTicked ( ticked ) ,
usesColour ( useColour ) , iconDrawable ( drawable ) ,
customComp ( custom ) , subMenu ( createCopyIfNotNull ( sub ) ) , commandManager ( manager )
{
if ( commandManager ! = nullptr & & itemID ! = 0 )
{
String shortcutKey ;
const Array < KeyPress > keyPresses ( commandManager - > getKeyMappings ( )
- > getKeyPressesAssignedToCommand ( itemID ) ) ;
for ( int i = 0 ; i < keyPresses . size ( ) ; + + i )
{
const String key ( keyPresses . getReference ( i ) . getTextDescriptionWithIcons ( ) ) ;
if ( shortcutKey . isNotEmpty ( ) )
shortcutKey < < " , " ;
if ( key . length ( ) = = 1 & & key [ 0 ] < 128 )
shortcutKey < < " shortcut: ' " < < key < < ' \' ' ;
else
shortcutKey < < key ;
}
shortcutKey = shortcutKey . trim ( ) ;
if ( shortcutKey . isNotEmpty ( ) )
text < < " <end> " < < shortcutKey ;
}
}
Item ( const Item & other )
: itemID ( other . itemID ) ,
text ( other . text ) ,
textColour ( other . textColour ) ,
isActive ( other . isActive ) ,
isSeparator ( other . isSeparator ) ,
isTicked ( other . isTicked ) ,
usesColour ( other . usesColour ) ,
iconDrawable ( other . iconDrawable ! = nullptr ? other . iconDrawable - > createCopy ( ) : nullptr ) ,
customComp ( other . customComp ) ,
subMenu ( createCopyIfNotNull ( other . subMenu . get ( ) ) ) ,
commandManager ( other . commandManager )
{ }
bool canBeTriggered ( ) const noexcept { return isActive & & itemID ! = 0 & & itemID ! = PopupMenuSettings : : sectionHeaderID ; }
bool hasActiveSubMenu ( ) const noexcept { return isActive & & subMenu ! = nullptr & & subMenu - > items . size ( ) > 0 ; }
//==============================================================================
const int itemID ;
String text ;
const Colour textColour ;
const bool isActive , isSeparator , isTicked , usesColour ;
ScopedPointer < Drawable > iconDrawable ;
ReferenceCountedObjectPtr < CustomComponent > customComp ;
ScopedPointer < PopupMenu > subMenu ;
ApplicationCommandManager * const commandManager ;
private :
Item & operator = ( const Item & ) ;
JUCE_LEAK_DETECTOR ( Item )
} ;
//==============================================================================
struct PopupMenu : : HelperClasses
{
class MouseSourceState ;
class MenuWindow ;
//==============================================================================
class ItemComponent : public Component
{
public :
ItemComponent ( const PopupMenu : : Item & info , int standardItemHeight , MenuWindow & parent )
: itemInfo ( info ) ,
isHighlighted ( false )
{
addAndMakeVisible ( itemInfo . customComp ) ;
parent . addAndMakeVisible ( this ) ;
int itemW = 80 ;
int itemH = 16 ;
getIdealSize ( itemW , itemH , standardItemHeight ) ;
setSize ( itemW , jlimit ( 2 , 600 , itemH ) ) ;
addMouseListener ( & parent , false ) ;
}
~ ItemComponent ( )
{
removeChildComponent ( itemInfo . customComp ) ;
}
void getIdealSize ( int & idealWidth , int & idealHeight , const int standardItemHeight )
{
if ( itemInfo . customComp ! = nullptr )
itemInfo . customComp - > getIdealSize ( idealWidth , idealHeight ) ;
else
getLookAndFeel ( ) . getIdealPopupMenuItemSize ( itemInfo . text ,
itemInfo . isSeparator ,
standardItemHeight ,
idealWidth , idealHeight ) ;
}
void paint ( Graphics & g ) override
{
if ( itemInfo . customComp = = nullptr )
{
String mainText ( itemInfo . text ) ;
String endText ;
const int endIndex = mainText . indexOf ( " <end> " ) ;
if ( endIndex > = 0 )
{
endText = mainText . substring ( endIndex + 5 ) . trim ( ) ;
mainText = mainText . substring ( 0 , endIndex ) ;
}
getLookAndFeel ( )
. drawPopupMenuItem ( g , getLocalBounds ( ) ,
itemInfo . isSeparator ,
itemInfo . isActive ,
isHighlighted ,
itemInfo . isTicked ,
itemInfo . subMenu ! = nullptr & & ( itemInfo . itemID = = 0 | | itemInfo . subMenu - > getNumItems ( ) > 0 ) ,
mainText , endText ,
itemInfo . iconDrawable ,
itemInfo . usesColour ? & ( itemInfo . textColour ) : nullptr ) ;
}
}
void resized ( ) override
{
if ( Component * const child = getChildComponent ( 0 ) )
child - > setBounds ( getLocalBounds ( ) . reduced ( 2 , 0 ) ) ;
}
void setHighlighted ( bool shouldBeHighlighted )
{
shouldBeHighlighted = shouldBeHighlighted & & itemInfo . isActive ;
if ( isHighlighted ! = shouldBeHighlighted )
{
isHighlighted = shouldBeHighlighted ;
if ( itemInfo . customComp ! = nullptr )
itemInfo . customComp - > setHighlighted ( shouldBeHighlighted ) ;
repaint ( ) ;
}
}
PopupMenu : : Item itemInfo ;
private :
bool isHighlighted ;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR ( ItemComponent )
} ;
//==============================================================================
class MenuWindow : public Component
{
public :
MenuWindow ( const PopupMenu & menu , MenuWindow * const parentWindow ,
const Options & opts ,
const bool alignToRectangle ,
const bool shouldDismissOnMouseUp ,
ApplicationCommandManager * * const manager )
: Component ( " menu " ) ,
parent ( parentWindow ) ,
options ( opts ) ,
managerOfChosenCommand ( manager ) ,
componentAttachedTo ( options . targetComponent ) ,
hasBeenOver ( false ) ,
needsToScroll ( false ) ,
dismissOnMouseUp ( shouldDismissOnMouseUp ) ,
hideOnExit ( false ) ,
disableMouseMoves ( false ) ,
hasAnyJuceCompHadFocus ( false ) ,
numColumns ( 0 ) ,
contentHeight ( 0 ) ,
childYOffset ( 0 ) ,
windowCreationTime ( Time : : getMillisecondCounter ( ) ) ,
lastFocusedTime ( windowCreationTime ) ,
timeEnteredCurrentChildComp ( windowCreationTime )
{
setWantsKeyboardFocus ( false ) ;
setMouseClickGrabsKeyboardFocus ( false ) ;
setAlwaysOnTop ( true ) ;
setLookAndFeel ( parent ! = nullptr ? & ( parent - > getLookAndFeel ( ) )
: menu . lookAndFeel ) ;
setOpaque ( getLookAndFeel ( ) . findColour ( PopupMenu : : backgroundColourId ) . isOpaque ( )
| | ! Desktop : : canUseSemiTransparentWindows ( ) ) ;
for ( int i = 0 ; i < menu . items . size ( ) ; + + i )
{
PopupMenu : : Item * const item = menu . items . getUnchecked ( i ) ;
if ( i < menu . items . size ( ) - 1 | | ! item - > isSeparator )
items . add ( new ItemComponent ( * item , options . standardHeight , * this ) ) ;
}
calculateWindowPos ( options . targetArea , alignToRectangle ) ;
setTopLeftPosition ( windowPos . getPosition ( ) ) ;
updateYPositions ( ) ;
if ( options . visibleItemID ! = 0 )
{
const int y = options . targetArea . getY ( ) - windowPos . getY ( ) ;
ensureItemIsVisible ( options . visibleItemID ,
isPositiveAndBelow ( y , windowPos . getHeight ( ) ) ? y : - 1 ) ;
}
resizeToBestWindowPos ( ) ;
addToDesktop ( ComponentPeer : : windowIsTemporary
| ComponentPeer : : windowIgnoresKeyPresses
| getLookAndFeel ( ) . getMenuWindowFlags ( ) ) ;
getActiveWindows ( ) . add ( this ) ;
Desktop : : getInstance ( ) . addGlobalMouseListener ( this ) ;
}
~ MenuWindow ( )
{
getActiveWindows ( ) . removeFirstMatchingValue ( this ) ;
Desktop : : getInstance ( ) . removeGlobalMouseListener ( this ) ;
activeSubMenu = nullptr ;
items . clear ( ) ;
}
//==============================================================================
void paint ( Graphics & g ) override
{
if ( isOpaque ( ) )
g . fillAll ( Colours : : white ) ;
getLookAndFeel ( ) . drawPopupMenuBackground ( g , getWidth ( ) , getHeight ( ) ) ;
}
void paintOverChildren ( Graphics & g ) override
{
if ( canScroll ( ) )
{
LookAndFeel & lf = getLookAndFeel ( ) ;
if ( isTopScrollZoneActive ( ) )
lf . drawPopupMenuUpDownArrow ( g , getWidth ( ) , PopupMenuSettings : : scrollZone , true ) ;
if ( isBottomScrollZoneActive ( ) )
{
g . setOrigin ( 0 , getHeight ( ) - PopupMenuSettings : : scrollZone ) ;
lf . drawPopupMenuUpDownArrow ( g , getWidth ( ) , PopupMenuSettings : : scrollZone , false ) ;
}
}
}
//==============================================================================
// hide this and all sub-comps
void hide ( const PopupMenu : : Item * const item , const bool makeInvisible )
{
if ( isVisible ( ) )
{
WeakReference < Component > deletionChecker ( this ) ;
activeSubMenu = nullptr ;
currentChild = nullptr ;
if ( item ! = nullptr
& & item - > commandManager ! = nullptr
& & item - > itemID ! = 0 )
{
* managerOfChosenCommand = item - > commandManager ;
}
exitModalState ( item ! = nullptr ? item - > itemID : 0 ) ;
if ( makeInvisible & & ( deletionChecker ! = nullptr ) )
setVisible ( false ) ;
}
}
void dismissMenu ( const PopupMenu : : Item * const item )
{
if ( parent ! = nullptr )
{
parent - > dismissMenu ( item ) ;
}
else
{
if ( item ! = nullptr )
{
// need a copy of this on the stack as the one passed in will get deleted during this call
const PopupMenu : : Item mi ( * item ) ;
hide ( & mi , false ) ;
}
else
{
hide ( nullptr , false ) ;
}
}
}
//==============================================================================
bool keyPressed ( const KeyPress & key ) override
{
if ( key . isKeyCode ( KeyPress : : downKey ) )
{
selectNextItem ( 1 ) ;
}
else if ( key . isKeyCode ( KeyPress : : upKey ) )
{
selectNextItem ( - 1 ) ;
}
else if ( key . isKeyCode ( KeyPress : : leftKey ) )
{
if ( parent ! = nullptr )
{
Component : : SafePointer < MenuWindow > parentWindow ( parent ) ;
ItemComponent * currentChildOfParent = parentWindow - > currentChild ;
hide ( nullptr , true ) ;
if ( parentWindow ! = nullptr )
parentWindow - > setCurrentlyHighlightedChild ( currentChildOfParent ) ;
disableTimerUntilMouseMoves ( ) ;
}
else if ( componentAttachedTo ! = nullptr )
{
componentAttachedTo - > keyPressed ( key ) ;
}
}
else if ( key . isKeyCode ( KeyPress : : rightKey ) )
{
disableTimerUntilMouseMoves ( ) ;
if ( showSubMenuFor ( currentChild ) )
{
if ( isSubMenuVisible ( ) )
activeSubMenu - > selectNextItem ( 1 ) ;
}
else if ( componentAttachedTo ! = nullptr )
{
componentAttachedTo - > keyPressed ( key ) ;
}
}
else if ( key . isKeyCode ( KeyPress : : returnKey ) )
{
triggerCurrentlyHighlightedItem ( ) ;
}
else if ( key . isKeyCode ( KeyPress : : escapeKey ) )
{
dismissMenu ( nullptr ) ;
}
else
{
return false ;
}
return true ;
}
void inputAttemptWhenModal ( ) override
{
WeakReference < Component > deletionChecker ( this ) ;
for ( int i = mouseSourceStates . size ( ) ; - - i > = 0 ; )
{
mouseSourceStates . getUnchecked ( i ) - > timerCallback ( ) ;
if ( deletionChecker = = nullptr )
return ;
}
if ( ! isOverAnyMenu ( ) )
{
if ( componentAttachedTo ! = nullptr )
{
// 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.
const Point < int > mousePos ( componentAttachedTo - > getMouseXYRelative ( ) ) ;
if ( componentAttachedTo - > reallyContains ( mousePos , true ) )
{
postCommandMessage ( PopupMenuSettings : : dismissCommandId ) ; // dismiss asynchrounously
return ;
}
}
dismissMenu ( nullptr ) ;
}
}
void handleCommandMessage ( int commandId ) override
{
Component : : handleCommandMessage ( commandId ) ;
if ( commandId = = PopupMenuSettings : : dismissCommandId )
dismissMenu ( nullptr ) ;
}
//==============================================================================
void mouseMove ( const MouseEvent & e ) override { handleMouseEvent ( e ) ; }
void mouseDown ( const MouseEvent & e ) override { handleMouseEvent ( e ) ; }
void mouseDrag ( const MouseEvent & e ) override { handleMouseEvent ( e ) ; }
void mouseUp ( const MouseEvent & e ) override { handleMouseEvent ( e ) ; }
void mouseWheelMove ( const MouseEvent & , const MouseWheelDetails & wheel ) override
{
alterChildYPos ( roundToInt ( - 10.0f * wheel . deltaY * PopupMenuSettings : : scrollZone ) ) ;
}
void handleMouseEvent ( const MouseEvent & e )
{
getMouseState ( e . source ) . handleMouseEvent ( e ) ;
}
bool windowIsStillValid ( )
{
if ( ! isVisible ( ) )
return false ;
if ( componentAttachedTo ! = options . targetComponent )
{
dismissMenu ( nullptr ) ;
return false ;
}
if ( MenuWindow * currentlyModalWindow = dynamic_cast < MenuWindow * > ( Component : : getCurrentlyModalComponent ( ) ) )
if ( ! treeContains ( currentlyModalWindow ) )
return false ;
return true ;
}
static Array < MenuWindow * > & getActiveWindows ( )
{
static Array < MenuWindow * > activeMenuWindows ;
return activeMenuWindows ;
}
MouseSourceState & getMouseState ( MouseInputSource source )
{
for ( int i = mouseSourceStates . size ( ) ; - - i > = 0 ; )
{
MouseSourceState & ms = * mouseSourceStates . getUnchecked ( i ) ;
if ( ms . source = = source )
return ms ;
}
MouseSourceState * ms = new MouseSourceState ( * this , source ) ;
mouseSourceStates . add ( ms ) ;
return * ms ;
}
//==============================================================================
bool isOverAnyMenu ( ) const
{
return parent ! = nullptr ? parent - > isOverAnyMenu ( )
: isOverChildren ( ) ;
}
bool isOverChildren ( ) const
{
return isVisible ( )
& & ( isAnyMouseOver ( ) | | ( activeSubMenu ! = nullptr & & activeSubMenu - > isOverChildren ( ) ) ) ;
}
bool isAnyMouseOver ( ) const
{
for ( int i = 0 ; i < mouseSourceStates . size ( ) ; + + i )
if ( mouseSourceStates . getUnchecked ( i ) - > isOver ( ) )
return true ;
return false ;
}
bool treeContains ( const MenuWindow * const window ) const noexcept
{
const MenuWindow * mw = this ;
while ( mw - > parent ! = nullptr )
mw = mw - > parent ;
while ( mw ! = nullptr )
{
if ( mw = = window )
return true ;
mw = mw - > activeSubMenu ;
}
return false ;
}
bool doesAnyJuceCompHaveFocus ( )
{
bool anyFocused = Process : : isForegroundProcess ( ) ;
if ( anyFocused & & Component : : getCurrentlyFocusedComponent ( ) = = nullptr )
{
// 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 ;
}
}
}
return anyFocused ;
}
//==============================================================================
void calculateWindowPos ( const Rectangle < int > & target , const bool alignToRectangle )
{
const Rectangle < int > mon ( Desktop : : getInstance ( ) . getDisplays ( )
. getDisplayContaining ( target . getCentre ( ) )
# if JUCE_MAC
. userArea ) ;
# else
. totalArea ) ; // on windows, don't stop the menu overlapping the taskbar
# endif
const int maxMenuHeight = mon . getHeight ( ) - 24 ;
int x , y , widthToUse , heightToUse ;
layoutMenuItems ( mon . getWidth ( ) - 24 , maxMenuHeight , widthToUse , heightToUse ) ;
if ( alignToRectangle )
{
x = target . getX ( ) ;
const int spaceUnder = mon . getHeight ( ) - ( target . getBottom ( ) - mon . getY ( ) ) ;
const int spaceOver = target . getY ( ) - mon . getY ( ) ;
if ( heightToUse < spaceUnder - 30 | | spaceUnder > = spaceOver )
y = target . getBottom ( ) ;
else
y = target . getY ( ) - heightToUse ;
}
else
{
bool tendTowardsRight = target . getCentreX ( ) < mon . getCentreX ( ) ;
if ( parent ! = nullptr )
{
if ( parent - > parent ! = nullptr )
{
const bool parentGoingRight = ( parent - > getX ( ) + parent - > getWidth ( ) / 2
> parent - > parent - > getX ( ) + parent - > parent - > getWidth ( ) / 2 ) ;
if ( parentGoingRight & & target . getRight ( ) + widthToUse < mon . getRight ( ) - 4 )
tendTowardsRight = true ;
else if ( ( ! parentGoingRight ) & & target . getX ( ) > widthToUse + 4 )
tendTowardsRight = false ;
}
else if ( target . getRight ( ) + widthToUse < mon . getRight ( ) - 32 )
{
tendTowardsRight = true ;
}
}
const int biggestSpace = jmax ( mon . getRight ( ) - target . getRight ( ) ,
target . getX ( ) - mon . getX ( ) ) - 32 ;
if ( biggestSpace < widthToUse )
{
layoutMenuItems ( biggestSpace + target . getWidth ( ) / 3 , maxMenuHeight , widthToUse , heightToUse ) ;
if ( numColumns > 1 )
layoutMenuItems ( biggestSpace - 4 , maxMenuHeight , widthToUse , heightToUse ) ;
tendTowardsRight = ( mon . getRight ( ) - target . getRight ( ) ) > = ( target . getX ( ) - mon . getX ( ) ) ;
}
if ( tendTowardsRight )
x = jmin ( mon . getRight ( ) - widthToUse - 4 , target . getRight ( ) ) ;
else
x = jmax ( mon . getX ( ) + 4 , target . getX ( ) - widthToUse ) ;
y = target . getY ( ) ;
if ( target . getCentreY ( ) > mon . getCentreY ( ) )
y = jmax ( mon . getY ( ) , target . getBottom ( ) - heightToUse ) ;
}
x = jmax ( mon . getX ( ) + 1 , jmin ( mon . getRight ( ) - ( widthToUse + 6 ) , x ) ) ;
y = jmax ( mon . getY ( ) + 1 , jmin ( 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 = parent ! = nullptr
& & parent - > windowPos . intersects ( windowPos . expanded ( - 4 , - 4 ) ) ;
}
void layoutMenuItems ( const int maxMenuW , const int maxMenuH , int & width , int & height )
{
numColumns = 0 ;
contentHeight = 0 ;
int totalW ;
const int maximumNumColumns = options . maxColumns > 0 ? options . maxColumns : 7 ;
do
{
+ + numColumns ;
totalW = workOutBestSize ( maxMenuW ) ;
if ( totalW > maxMenuW )
{
numColumns = jmax ( 1 , numColumns - 1 ) ;
workOutBestSize ( 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 + PopupMenuSettings : : borderSize * 2 ;
}
int workOutBestSize ( const int maxMenuW )
{
int totalW = 0 ;
contentHeight = 0 ;
int childNum = 0 ;
for ( int col = 0 ; col < numColumns ; + + col )
{
int colW = options . standardHeight , colH = 0 ;
const int numChildren = jmin ( items . size ( ) - childNum ,
( items . size ( ) + numColumns - 1 ) / numColumns ) ;
for ( int i = numChildren ; - - i > = 0 ; )
{
colW = jmax ( colW , items . getUnchecked ( childNum + i ) - > getWidth ( ) ) ;
colH + = items . getUnchecked ( childNum + i ) - > getHeight ( ) ;
}
colW = jmin ( maxMenuW / jmax ( 1 , numColumns - 2 ) , colW + PopupMenuSettings : : borderSize * 2 ) ;
columnWidths . set ( col , colW ) ;
totalW + = colW ;
contentHeight = jmax ( contentHeight , colH ) ;
childNum + = numChildren ;
}
if ( totalW < options . minWidth )
{
totalW = options . minWidth ;
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 = items . size ( ) ; - - i > = 0 ; )
{
ItemComponent * const m = items . getUnchecked ( i ) ;
if ( m ! = nullptr
& & m - > itemInfo . itemID = = itemID
& & windowPos . getHeight ( ) > PopupMenuSettings : : scrollZone * 4 )
{
const int currentY = m - > getY ( ) ;
if ( wantedY > 0 | | currentY < 0 | | m - > getBottom ( ) > windowPos . getHeight ( ) )
{
if ( wantedY < 0 )
wantedY = jlimit ( PopupMenuSettings : : scrollZone ,
jmax ( PopupMenuSettings : : scrollZone ,
windowPos . getHeight ( ) - ( PopupMenuSettings : : scrollZone + m - > getHeight ( ) ) ) ,
currentY ) ;
const Rectangle < int > mon ( Desktop : : getInstance ( ) . getDisplays ( )
. getDisplayContaining ( windowPos . getPosition ( ) ) . userArea ) ;
int deltaY = wantedY - currentY ;
windowPos . setSize ( jmin ( windowPos . getWidth ( ) , mon . getWidth ( ) ) ,
jmin ( windowPos . getHeight ( ) , mon . getHeight ( ) ) ) ;
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 < int > r ( windowPos ) ;
if ( childYOffset < 0 )
{
r = r . withTop ( r . getY ( ) - 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 ( canScroll ( ) )
{
childYOffset + = delta ;
if ( delta < 0 )
childYOffset = jmax ( childYOffset , 0 ) ;
else if ( delta > 0 )
childYOffset = jmin ( childYOffset ,
contentHeight - windowPos . getHeight ( ) + PopupMenuSettings : : 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 ( items . size ( ) - childNum ,
( items . size ( ) + numColumns - 1 ) / numColumns ) ;
const int colW = columnWidths [ col ] ;
int y = PopupMenuSettings : : borderSize - ( childYOffset + ( getY ( ) - windowPos . getY ( ) ) ) ;
for ( int i = 0 ; i < numChildren ; + + i )
{
Component * const c = items . getUnchecked ( childNum + i ) ;
c - > setBounds ( x , y , colW , c - > getHeight ( ) ) ;
y + = c - > getHeight ( ) ;
}
x + = colW ;
childNum + = numChildren ;
}
return x ;
}
void setCurrentlyHighlightedChild ( ItemComponent * const child )
{
if ( currentChild ! = nullptr )
currentChild - > setHighlighted ( false ) ;
currentChild = child ;
if ( currentChild ! = nullptr )
{
currentChild - > setHighlighted ( true ) ;
timeEnteredCurrentChildComp = Time : : getApproximateMillisecondCounter ( ) ;
}
}
bool isSubMenuVisible ( ) const noexcept { return activeSubMenu ! = nullptr & & activeSubMenu - > isVisible ( ) ; }
bool showSubMenuFor ( ItemComponent * const childComp )
{
activeSubMenu = nullptr ;
if ( childComp ! = nullptr
& & childComp - > itemInfo . hasActiveSubMenu ( ) )
{
activeSubMenu = new HelperClasses : : MenuWindow ( * ( childComp - > itemInfo . subMenu ) , this ,
options . withTargetScreenArea ( childComp - > getScreenBounds ( ) )
. withMinimumWidth ( 0 )
. withTargetComponent ( nullptr ) ,
false , dismissOnMouseUp , managerOfChosenCommand ) ;
activeSubMenu - > setVisible ( true ) ; // (must be called before enterModalState on Windows to avoid DropShadower confusion)
activeSubMenu - > enterModalState ( false ) ;
activeSubMenu - > toFront ( false ) ;
return true ;
}
return false ;
}
void triggerCurrentlyHighlightedItem ( )
{
if ( currentChild ! = nullptr
& & currentChild - > itemInfo . canBeTriggered ( )
& & ( currentChild - > itemInfo . customComp = = nullptr
| | currentChild - > itemInfo . customComp - > isTriggeredAutomatically ( ) ) )
{
dismissMenu ( & currentChild - > itemInfo ) ;
}
}
void selectNextItem ( const int delta )
{
disableTimerUntilMouseMoves ( ) ;
int start = jmax ( 0 , items . indexOf ( currentChild ) ) ;
for ( int i = items . size ( ) ; - - i > = 0 ; )
{
start + = delta ;
if ( ItemComponent * mic = items . getUnchecked ( ( start + items . size ( ) ) % items . size ( ) ) )
{
if ( mic - > itemInfo . canBeTriggered ( ) | | mic - > itemInfo . hasActiveSubMenu ( ) )
{
setCurrentlyHighlightedChild ( mic ) ;
break ;
}
}
}
}
void disableTimerUntilMouseMoves ( )
{
disableMouseMoves = true ;
if ( parent ! = nullptr )
parent - > disableTimerUntilMouseMoves ( ) ;
}
bool canScroll ( ) const noexcept { return childYOffset ! = 0 | | needsToScroll ; }
bool isTopScrollZoneActive ( ) const noexcept { return canScroll ( ) & & childYOffset > 0 ; }
bool isBottomScrollZoneActive ( ) const noexcept { return canScroll ( ) & & childYOffset < contentHeight - windowPos . getHeight ( ) ; }
//==============================================================================
MenuWindow * parent ;
const Options options ;
OwnedArray < ItemComponent > items ;
ApplicationCommandManager * * managerOfChosenCommand ;
WeakReference < Component > componentAttachedTo ;
Rectangle < int > windowPos ;
bool hasBeenOver , needsToScroll ;
bool dismissOnMouseUp , hideOnExit , disableMouseMoves , hasAnyJuceCompHadFocus ;
int numColumns , contentHeight , childYOffset ;
Component : : SafePointer < ItemComponent > currentChild ;
ScopedPointer < MenuWindow > activeSubMenu ;
Array < int > columnWidths ;
uint32 windowCreationTime , lastFocusedTime , timeEnteredCurrentChildComp ;
OwnedArray < MouseSourceState > mouseSourceStates ;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR ( MenuWindow )
} ;
//==============================================================================
class MouseSourceState : private Timer
{
public :
MouseSourceState ( MenuWindow & w , MouseInputSource s )
: window ( w ) , source ( s ) , scrollAcceleration ( 1.0 ) ,
lastScrollTime ( Time : : getMillisecondCounter ( ) ) ,
lastMouseMoveTime ( 0 ) , isDown ( false )
{
}
void handleMouseEvent ( const MouseEvent & e )
{
if ( ! window . windowIsStillValid ( ) )
return ;
startTimer ( PopupMenuSettings : : timerInterval ) ;
handleMousePosition ( e . getScreenPosition ( ) ) ;
}
void timerCallback ( ) override
{
if ( window . windowIsStillValid ( ) )
handleMousePosition ( source . getScreenPosition ( ) . roundToInt ( ) ) ;
}
bool isOver ( ) const
{
return window . reallyContains ( window . getLocalPoint ( nullptr , source . getScreenPosition ( ) ) . roundToInt ( ) , true ) ;
}
MenuWindow & window ;
MouseInputSource source ;
private :
Point < int > lastMousePos ;
double scrollAcceleration ;
uint32 lastScrollTime , lastMouseMoveTime ;
bool isDown ;
void handleMousePosition ( Point < int > globalMousePos )
{
const Point < int > localMousePos ( window . getLocalPoint ( nullptr , globalMousePos ) ) ;
const uint32 timeNow = Time : : getMillisecondCounter ( ) ;
if ( timeNow > window . timeEnteredCurrentChildComp + 100
& & window . reallyContains ( localMousePos , true )
& & window . currentChild ! = nullptr
& & ! ( window . disableMouseMoves | | window . isSubMenuVisible ( ) ) )
{
window . showSubMenuFor ( window . currentChild ) ;
}
highlightItemUnderMouse ( globalMousePos , localMousePos , timeNow ) ;
const bool overScrollArea = scrollIfNecessary ( localMousePos , timeNow ) ;
const bool isOverAny = window . isOverAnyMenu ( ) ;
if ( window . hideOnExit & & window . hasBeenOver & & ! isOverAny )
window . hide ( nullptr , true ) ;
else
checkButtonState ( localMousePos , timeNow , isDown , overScrollArea , isOverAny ) ;
}
void checkButtonState ( Point < int > localMousePos , const uint32 timeNow ,
const bool wasDown , const bool overScrollArea , const bool isOverAny )
{
isDown = window . hasBeenOver
& & ( ModifierKeys : : getCurrentModifiers ( ) . isAnyMouseButtonDown ( )
| | ModifierKeys : : getCurrentModifiersRealtime ( ) . isAnyMouseButtonDown ( ) ) ;
if ( ! window . doesAnyJuceCompHaveFocus ( ) )
{
if ( timeNow > window . lastFocusedTime + 10 )
{
PopupMenuSettings : : menuWasHiddenBecauseOfAppChange = true ;
window . dismissMenu ( nullptr ) ;
// Note: this object may have been deleted by the previous call..
}
}
else if ( wasDown & & timeNow > window . windowCreationTime + 250
& & ! ( isDown | | overScrollArea ) )
{
if ( window . reallyContains ( localMousePos , true ) )
window . triggerCurrentlyHighlightedItem ( ) ;
else if ( ( window . hasBeenOver | | ! window . dismissOnMouseUp ) & & ! isOverAny )
window . dismissMenu ( nullptr ) ;
// Note: this object may have been deleted by the previous call..
}
else
{
window . lastFocusedTime = timeNow ;
}
}
void highlightItemUnderMouse ( Point < int > globalMousePos , Point < int > localMousePos , const uint32 timeNow )
{
if ( globalMousePos ! = lastMousePos | | timeNow > lastMouseMoveTime + 350 )
{
const bool isMouseOver = window . reallyContains ( localMousePos , true ) ;
if ( isMouseOver )
window . hasBeenOver = true ;
if ( lastMousePos . getDistanceFrom ( globalMousePos ) > 2 )
{
lastMouseMoveTime = timeNow ;
if ( window . disableMouseMoves & & isMouseOver )
window . disableMouseMoves = false ;
}
if ( window . disableMouseMoves | | ( window . activeSubMenu ! = nullptr & & window . activeSubMenu - > isOverChildren ( ) ) )
return ;
const bool isMovingTowardsMenu = isMouseOver & & globalMousePos ! = lastMousePos
& & isMovingTowardsSubmenu ( globalMousePos ) ;
lastMousePos = globalMousePos ;
if ( ! isMovingTowardsMenu )
{
Component * c = window . getComponentAt ( localMousePos ) ;
if ( c = = & window )
c = nullptr ;
ItemComponent * itemUnderMouse = dynamic_cast < ItemComponent * > ( c ) ;
if ( itemUnderMouse = = nullptr & & c ! = nullptr )
itemUnderMouse = c - > findParentComponentOfClass < ItemComponent > ( ) ;
if ( itemUnderMouse ! = window . currentChild
& & ( isMouseOver | | ( window . activeSubMenu = = nullptr ) | | ! window . activeSubMenu - > isVisible ( ) ) )
{
if ( isMouseOver & & ( c ! = nullptr ) & & ( window . activeSubMenu ! = nullptr ) )
window . activeSubMenu - > hide ( nullptr , true ) ;
if ( ! isMouseOver )
itemUnderMouse = nullptr ;
window . setCurrentlyHighlightedChild ( itemUnderMouse ) ;
}
}
}
}
bool isMovingTowardsSubmenu ( Point < int > newGlobalPos ) const
{
if ( window . activeSubMenu = = nullptr )
return false ;
// 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..
const Rectangle < int > itemScreenBounds ( window . activeSubMenu - > getScreenBounds ( ) ) ;
float subX = ( float ) itemScreenBounds . getX ( ) ;
Point < int > oldGlobalPos ( lastMousePos ) ;
if ( itemScreenBounds . getX ( ) > window . getX ( ) )
{
oldGlobalPos - = Point < int > ( 2 , 0 ) ; // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels
}
else
{
oldGlobalPos + = Point < int > ( 2 , 0 ) ;
subX + = itemScreenBounds . getWidth ( ) ;
}
Path areaTowardsSubMenu ;
areaTowardsSubMenu . addTriangle ( ( float ) oldGlobalPos . x , ( float ) oldGlobalPos . y ,
subX , ( float ) itemScreenBounds . getY ( ) ,
subX , ( float ) itemScreenBounds . getBottom ( ) ) ;
return areaTowardsSubMenu . contains ( newGlobalPos . toFloat ( ) ) ;
}
bool scrollIfNecessary ( Point < int > localMousePos , const uint32 timeNow )
{
if ( window . canScroll ( )
& & isPositiveAndBelow ( localMousePos . x , window . getWidth ( ) )
& & ( isPositiveAndBelow ( localMousePos . y , window . getHeight ( ) ) | | source . isDragging ( ) ) )
{
if ( window . isTopScrollZoneActive ( ) & & localMousePos . y < PopupMenuSettings : : scrollZone )
return scroll ( timeNow , - 1 ) ;
if ( window . isBottomScrollZoneActive ( ) & & localMousePos . y > window . getHeight ( ) - PopupMenuSettings : : scrollZone )
return scroll ( timeNow , 1 ) ;
}
scrollAcceleration = 1.0 ;
return false ;
}
bool scroll ( const uint32 timeNow , const int direction )
{
if ( timeNow > lastScrollTime + 20 )
{
scrollAcceleration = jmin ( 4.0 , scrollAcceleration * 1.04 ) ;
int amount = 0 ;
for ( int i = 0 ; i < window . items . size ( ) & & amount = = 0 ; + + i )
amount = ( ( int ) scrollAcceleration ) * window . items . getUnchecked ( i ) - > getHeight ( ) ;
window . alterChildYPos ( amount * direction ) ;
lastScrollTime = timeNow ;
}
return true ;
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR ( MouseSourceState )
} ;
//==============================================================================
class NormalComponentWrapper : public PopupMenu : : CustomComponent
{
public :
NormalComponentWrapper ( Component * const comp , const int w , const int h ,
const bool triggerMenuItemAutomaticallyWhenClicked )
: PopupMenu : : CustomComponent ( triggerMenuItemAutomaticallyWhenClicked ) ,
width ( w ) , height ( h )
{
addAndMakeVisible ( comp ) ;
}
void getIdealSize ( int & idealWidth , int & idealHeight ) override
{
idealWidth = width ;
idealHeight = height ;
}
void resized ( ) override
{
if ( Component * const child = getChildComponent ( 0 ) )
child - > setBounds ( getLocalBounds ( ) ) ;
}
private :
const int width , height ;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR ( NormalComponentWrapper )
} ;
//==============================================================================
class HeaderItemComponent : public PopupMenu : : CustomComponent
{
public :
HeaderItemComponent ( const String & name )
: PopupMenu : : CustomComponent ( false )
{
setName ( name ) ;
}
void paint ( Graphics & g ) override
{
getLookAndFeel ( ) . drawPopupMenuSectionHeader ( g , getLocalBounds ( ) , getName ( ) ) ;
}
void getIdealSize ( int & idealWidth , int & idealHeight ) override
{
getLookAndFeel ( ) . getIdealPopupMenuItemSize ( getName ( ) , false , - 1 , idealWidth , idealHeight ) ;
idealHeight + = idealHeight / 2 ;
idealWidth + = idealWidth / 4 ;
}
private :
JUCE_LEAK_DETECTOR ( HeaderItemComponent )
} ;
} ;
//==============================================================================
PopupMenu : : PopupMenu ( )
: lookAndFeel ( nullptr )
{
}
PopupMenu : : PopupMenu ( const PopupMenu & other )
: lookAndFeel ( other . lookAndFeel )
{
items . addCopiesOf ( other . items ) ;
}
PopupMenu & PopupMenu : : operator = ( const PopupMenu & other )
{
if ( this ! = & other )
{
lookAndFeel = other . lookAndFeel ;
clear ( ) ;
items . addCopiesOf ( other . items ) ;
}
return * this ;
}
# if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
PopupMenu : : PopupMenu ( PopupMenu & & other ) noexcept
: lookAndFeel ( other . lookAndFeel )
{
items . swapWith ( other . items ) ;
}
PopupMenu & PopupMenu : : operator = ( PopupMenu & & other ) noexcept
{
jassert ( this ! = & other ) ; // hopefully the compiler should make this situation impossible!
items . swapWith ( other . items ) ;
lookAndFeel = other . lookAndFeel ;
return * this ;
}
# endif
PopupMenu : : ~ PopupMenu ( )
{
}
void PopupMenu : : clear ( )
{
items . clear ( ) ;
}
void PopupMenu : : addItem ( int itemResultID , const String & itemText , bool isActive , bool isTicked )
{
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..
items . add ( new Item ( itemResultID , itemText , isActive , isTicked , nullptr ,
Colours : : black , false , nullptr , nullptr , nullptr ) ) ;
}
static Drawable * createDrawableFromImage ( const Image & im )
{
if ( im . isValid ( ) )
{
DrawableImage * d = new DrawableImage ( ) ;
d - > setImage ( im ) ;
return d ;
}
return nullptr ;
}
void PopupMenu : : addItem ( int itemResultID , const String & itemText , bool isActive , bool isTicked , const Image & iconToUse )
{
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..
items . add ( new Item ( itemResultID , itemText , isActive , isTicked , createDrawableFromImage ( iconToUse ) ,
Colours : : black , false , nullptr , nullptr , nullptr ) ) ;
}
void PopupMenu : : addItem ( int itemResultID , const String & itemText , bool isActive , bool isTicked , Drawable * iconToUse )
{
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..
items . add ( new Item ( itemResultID , itemText , isActive , isTicked , iconToUse ,
Colours : : black , false , nullptr , nullptr , nullptr ) ) ;
}
void PopupMenu : : addCommandItem ( ApplicationCommandManager * commandManager ,
const CommandID commandID ,
const String & displayName )
{
jassert ( commandManager ! = nullptr & & commandID ! = 0 ) ;
if ( const ApplicationCommandInfo * const registeredInfo = commandManager - > getCommandForID ( commandID ) )
{
ApplicationCommandInfo info ( * registeredInfo ) ;
ApplicationCommandTarget * const target = commandManager - > getTargetForCommand ( commandID , info ) ;
items . add ( new Item ( commandID ,
displayName . isNotEmpty ( ) ? displayName
: info . shortName ,
target ! = nullptr & & ( info . flags & ApplicationCommandInfo : : isDisabled ) = = 0 ,
( info . flags & ApplicationCommandInfo : : isTicked ) ! = 0 ,
nullptr ,
Colours : : black ,
false ,
nullptr , nullptr ,
commandManager ) ) ;
}
}
void PopupMenu : : addColouredItem ( int itemResultID , const String & itemText , Colour itemTextColour ,
bool isActive , bool isTicked , const Image & iconToUse )
{
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..
items . add ( new Item ( itemResultID , itemText , isActive , isTicked , createDrawableFromImage ( iconToUse ) ,
itemTextColour , true , nullptr , nullptr , nullptr ) ) ;
}
void PopupMenu : : addCustomItem ( int itemID , CustomComponent * cc , const PopupMenu * subMenu )
{
jassert ( itemID ! = 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..
items . add ( new Item ( itemID , String : : empty , true , false , nullptr ,
Colours : : black , false , cc , subMenu , nullptr ) ) ;
}
void PopupMenu : : addCustomItem ( int itemResultID , Component * customComponent , int idealWidth , int idealHeight ,
bool triggerMenuItemAutomaticallyWhenClicked , const PopupMenu * subMenu )
{
items . add ( new Item ( itemResultID , String : : empty , true , false , nullptr , Colours : : black , false ,
new HelperClasses : : NormalComponentWrapper ( customComponent , idealWidth , idealHeight ,
triggerMenuItemAutomaticallyWhenClicked ) ,
subMenu , nullptr ) ) ;
}
void PopupMenu : : addSubMenu ( const String & subMenuName , const PopupMenu & subMenu , bool isActive )
{
addSubMenu ( subMenuName , subMenu , isActive , nullptr , false , 0 ) ;
}
void PopupMenu : : addSubMenu ( const String & subMenuName , const PopupMenu & subMenu , bool isActive ,
const Image & iconToUse , bool isTicked , int itemResultID )
{
addSubMenu ( subMenuName , subMenu , isActive , createDrawableFromImage ( iconToUse ) , isTicked , itemResultID ) ;
}
void PopupMenu : : addSubMenu ( const String & subMenuName , const PopupMenu & subMenu , bool isActive ,
Drawable * iconToUse , bool isTicked , int itemResultID )
{
items . add ( new Item ( itemResultID , subMenuName , isActive & & ( itemResultID ! = 0 | | subMenu . getNumItems ( ) > 0 ) , isTicked ,
iconToUse , Colours : : black , false , nullptr , & subMenu , nullptr ) ) ;
}
void PopupMenu : : addSeparator ( )
{
if ( items . size ( ) > 0 & & ! items . getLast ( ) - > isSeparator )
items . add ( new Item ( ) ) ;
}
void PopupMenu : : addSectionHeader ( const String & title )
{
addCustomItem ( PopupMenuSettings : : sectionHeaderID , new HelperClasses : : HeaderItemComponent ( title ) ) ;
}
//==============================================================================
PopupMenu : : Options : : Options ( )
: targetComponent ( nullptr ) ,
visibleItemID ( 0 ) ,
minWidth ( 0 ) ,
maxColumns ( 0 ) ,
standardHeight ( 0 )
{
targetArea . setPosition ( Desktop : : getMousePosition ( ) ) ;
}
PopupMenu : : Options PopupMenu : : Options : : withTargetComponent ( Component * comp ) const noexcept
{
Options o ( * this ) ;
o . targetComponent = comp ;
if ( comp ! = nullptr )
o . targetArea = comp - > getScreenBounds ( ) ;
return o ;
}
PopupMenu : : Options PopupMenu : : Options : : withTargetScreenArea ( const Rectangle < int > & area ) const noexcept
{
Options o ( * this ) ;
o . targetArea = area ;
return o ;
}
PopupMenu : : Options PopupMenu : : Options : : withMinimumWidth ( int w ) const noexcept
{
Options o ( * this ) ;
o . minWidth = w ;
return o ;
}
PopupMenu : : Options PopupMenu : : Options : : withMaximumNumColumns ( int cols ) const noexcept
{
Options o ( * this ) ;
o . maxColumns = cols ;
return o ;
}
PopupMenu : : Options PopupMenu : : Options : : withStandardItemHeight ( int height ) const noexcept
{
Options o ( * this ) ;
o . standardHeight = height ;
return o ;
}
PopupMenu : : Options PopupMenu : : Options : : withItemThatMustBeVisible ( int idOfItemToBeVisible ) const noexcept
{
Options o ( * this ) ;
o . visibleItemID = idOfItemToBeVisible ;
return o ;
}
Component * PopupMenu : : createWindow ( const Options & options ,
ApplicationCommandManager * * managerOfChosenCommand ) const
{
if ( items . size ( ) > 0 )
return new HelperClasses : : MenuWindow ( * this , nullptr , options ,
! options . targetArea . isEmpty ( ) ,
ModifierKeys : : getCurrentModifiers ( ) . isAnyMouseButtonDown ( ) ,
managerOfChosenCommand ) ;
return nullptr ;
}
//==============================================================================
// This invokes any command manager commands and deletes the menu window when it is dismissed
class PopupMenuCompletionCallback : public ModalComponentManager : : Callback
{
public :
PopupMenuCompletionCallback ( )
: managerOfChosenCommand ( nullptr ) ,
prevFocused ( Component : : getCurrentlyFocusedComponent ( ) ) ,
prevTopLevel ( prevFocused ! = nullptr ? prevFocused - > getTopLevelComponent ( ) : nullptr )
{
PopupMenuSettings : : menuWasHiddenBecauseOfAppChange = false ;
}
void modalStateFinished ( int result )
{
if ( managerOfChosenCommand ! = nullptr & & result ! = 0 )
{
ApplicationCommandTarget : : InvocationInfo info ( result ) ;
info . invocationMethod = ApplicationCommandTarget : : InvocationInfo : : fromMenu ;
managerOfChosenCommand - > invoke ( info , true ) ;
}
// (this would be the place to fade out the component, if that's what's required)
component = nullptr ;
if ( ! PopupMenuSettings : : menuWasHiddenBecauseOfAppChange )
{
if ( prevTopLevel ! = nullptr )
prevTopLevel - > toFront ( true ) ;
if ( prevFocused ! = nullptr )
prevFocused - > grabKeyboardFocus ( ) ;
}
}
ApplicationCommandManager * managerOfChosenCommand ;
ScopedPointer < Component > component ;
WeakReference < Component > prevFocused , prevTopLevel ;
private :
JUCE_DECLARE_NON_COPYABLE ( PopupMenuCompletionCallback )
} ;
int PopupMenu : : showWithOptionalCallback ( const Options & options , ModalComponentManager : : Callback * const userCallback ,
const bool canBeModal )
{
ScopedPointer < ModalComponentManager : : Callback > userCallbackDeleter ( userCallback ) ;
ScopedPointer < PopupMenuCompletionCallback > callback ( new PopupMenuCompletionCallback ( ) ) ;
if ( Component * window = createWindow ( options , & ( callback - > managerOfChosenCommand ) ) )
{
callback - > component = window ;
window - > setVisible ( true ) ; // (must be called before enterModalState on Windows to avoid DropShadower confusion)
window - > enterModalState ( false , userCallbackDeleter . release ( ) ) ;
ModalComponentManager : : getInstance ( ) - > attachCallback ( window , callback . release ( ) ) ;
window - > toFront ( false ) ; // need to do this after making it modal, or it could
// be stuck behind other comps that are already modal..
# if JUCE_MODAL_LOOPS_PERMITTED
if ( userCallback = = nullptr & & canBeModal )
return window - > runModalLoop ( ) ;
# else
( void ) canBeModal ;
jassert ( ! ( userCallback = = nullptr & & canBeModal ) ) ;
# endif
}
return 0 ;
}
//==============================================================================
# if JUCE_MODAL_LOOPS_PERMITTED
int PopupMenu : : showMenu ( const Options & options )
{
return showWithOptionalCallback ( options , nullptr , true ) ;
}
# endif
void PopupMenu : : showMenuAsync ( const Options & options , ModalComponentManager : : Callback * userCallback )
{
# if ! JUCE_MODAL_LOOPS_PERMITTED
jassert ( userCallback ! = nullptr ) ;
# endif
showWithOptionalCallback ( options , userCallback , false ) ;
}
//==============================================================================
# if JUCE_MODAL_LOOPS_PERMITTED
int PopupMenu : : show ( const int itemIDThatMustBeVisible ,
const int minimumWidth , const int maximumNumColumns ,
const int standardItemHeight ,
ModalComponentManager : : Callback * callback )
{
return showWithOptionalCallback ( Options ( ) . withItemThatMustBeVisible ( itemIDThatMustBeVisible )
. withMinimumWidth ( minimumWidth )
. withMaximumNumColumns ( maximumNumColumns )
. withStandardItemHeight ( standardItemHeight ) ,
callback , true ) ;
}
int PopupMenu : : showAt ( const Rectangle < int > & screenAreaToAttachTo ,
const int itemIDThatMustBeVisible ,
const int minimumWidth , const int maximumNumColumns ,
const int standardItemHeight ,
ModalComponentManager : : Callback * callback )
{
return showWithOptionalCallback ( Options ( ) . withTargetScreenArea ( screenAreaToAttachTo )
. withItemThatMustBeVisible ( itemIDThatMustBeVisible )
. withMinimumWidth ( minimumWidth )
. withMaximumNumColumns ( maximumNumColumns )
. withStandardItemHeight ( standardItemHeight ) ,
callback , true ) ;
}
int PopupMenu : : showAt ( Component * componentToAttachTo ,
const int itemIDThatMustBeVisible ,
const int minimumWidth , const int maximumNumColumns ,
const int standardItemHeight ,
ModalComponentManager : : Callback * callback )
{
Options options ( Options ( ) . withItemThatMustBeVisible ( itemIDThatMustBeVisible )
. withMinimumWidth ( minimumWidth )
. withMaximumNumColumns ( maximumNumColumns )
. withStandardItemHeight ( standardItemHeight ) ) ;
if ( componentToAttachTo ! = nullptr )
options = options . withTargetComponent ( componentToAttachTo ) ;
return showWithOptionalCallback ( options , callback , true ) ;
}
# endif
bool JUCE_CALLTYPE PopupMenu : : dismissAllActiveMenus ( )
{
const Array < HelperClasses : : MenuWindow * > & windows = HelperClasses : : MenuWindow : : getActiveWindows ( ) ;
const int numWindows = windows . size ( ) ;
for ( int i = numWindows ; - - i > = 0 ; )
if ( HelperClasses : : MenuWindow * const pmw = windows [ i ] )
pmw - > dismissMenu ( nullptr ) ;
return numWindows > 0 ;
}
//==============================================================================
int PopupMenu : : getNumItems ( ) const noexcept
{
int num = 0 ;
for ( int i = items . size ( ) ; - - i > = 0 ; )
if ( ! items . getUnchecked ( i ) - > isSeparator )
+ + num ;
return num ;
}
bool PopupMenu : : containsCommandItem ( const int commandID ) const
{
for ( int i = items . size ( ) ; - - i > = 0 ; )
{
const Item & mi = * items . getUnchecked ( i ) ;
if ( ( mi . itemID = = commandID & & mi . commandManager ! = nullptr )
| | ( mi . subMenu ! = nullptr & & mi . subMenu - > containsCommandItem ( commandID ) ) )
{
return true ;
}
}
return false ;
}
bool PopupMenu : : containsAnyActiveItems ( ) const noexcept
{
for ( int i = items . size ( ) ; - - i > = 0 ; )
{
const Item & mi = * items . getUnchecked ( i ) ;
if ( mi . subMenu ! = nullptr )
{
if ( mi . subMenu - > containsAnyActiveItems ( ) )
return true ;
}
else if ( mi . isActive )
{
return true ;
}
}
return false ;
}
void PopupMenu : : setLookAndFeel ( LookAndFeel * const newLookAndFeel )
{
lookAndFeel = newLookAndFeel ;
}
//==============================================================================
PopupMenu : : CustomComponent : : CustomComponent ( bool autoTrigger )
: isHighlighted ( false ) ,
triggeredAutomatically ( autoTrigger )
{
}
PopupMenu : : CustomComponent : : ~ CustomComponent ( )
{
}
void PopupMenu : : CustomComponent : : setHighlighted ( bool shouldBeHighlighted )
{
isHighlighted = shouldBeHighlighted ;
repaint ( ) ;
}
void PopupMenu : : CustomComponent : : triggerMenuItem ( )
{
if ( HelperClasses : : ItemComponent * const mic = findParentComponentOfClass < HelperClasses : : ItemComponent > ( ) )
{
if ( HelperClasses : : MenuWindow * const pmw = mic - > findParentComponentOfClass < HelperClasses : : MenuWindow > ( ) )
{
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 & m )
: subMenu ( nullptr ) ,
itemId ( 0 ) ,
isSeparator ( false ) ,
isTicked ( false ) ,
isEnabled ( false ) ,
isCustomComponent ( false ) ,
isSectionHeader ( false ) ,
customColour ( nullptr ) ,
menu ( m ) ,
index ( 0 )
{
}
PopupMenu : : MenuItemIterator : : ~ MenuItemIterator ( )
{
}
bool PopupMenu : : MenuItemIterator : : next ( )
{
if ( index > = menu . items . size ( ) )
return false ;
const Item * const item = menu . items . getUnchecked ( index ) ;
+ + index ;
if ( item - > isSeparator & & index > = menu . items . size ( ) ) // (avoid showing a separator at the end)
return false ;
itemName = item - > customComp ! = nullptr ? item - > customComp - > getName ( ) : item - > text ;
subMenu = item - > subMenu ;
itemId = item - > itemID ;
isSeparator = item - > isSeparator ;
isTicked = item - > isTicked ;
isEnabled = item - > isActive ;
isSectionHeader = dynamic_cast < HelperClasses : : HeaderItemComponent * > ( static_cast < CustomComponent * > ( item - > customComp ) ) ! = nullptr ;
isCustomComponent = ( ! isSectionHeader ) & & item - > customComp ! = nullptr ;
customColour = item - > usesColour ? & ( item - > textColour ) : nullptr ;
icon = item - > iconDrawable ;
commandManager = item - > commandManager ;
return true ;
}
void PopupMenu : : MenuItemIterator : : addItemTo ( PopupMenu & targetMenu )
{
targetMenu . items . add ( new Item ( itemId , itemName , isEnabled , isTicked , icon ! = nullptr ? icon - > createCopy ( ) : nullptr ,
customColour ! = nullptr ? * customColour : Colours : : black ,
customColour ! = nullptr , nullptr , subMenu , commandManager ) ) ;
}