The purpose of this guide is to aid authors in creating simple dropdown menus that are using the default UI's built in UIDropDownMenu without requiring any library.
Limitations of UIDropDownMenu
No sliders.
Cannot refresh a menu. You need to close the current menu level and get the user to reopen it to redraw it.
No editboxes. You can however use a staticpopup to achieve it.
No confirmations. You can however use a staticpopup to achieve it.
No tooltips if "Beginner Tooltips" cvar is turned off.
Only .checked menu item attribute is allowed to be of type function, which means it gets called to get a return value of true/false. The other attributes cannot be assigned functions to obtain their values.
Advantages of UIDropDownMenu
Comes with default UI, so you do not need to include large libraries like DewdropLib.
Programmatic creation of menu items, so they are dynamic. AceOption tables are fairly static in nature unless you have code to modify the aceopt tables directly.
Very low overhead. You only need to create one frame and one menu initialization function.
Some Terminology
Look at the following dropdown menus that are created by using UIDropDownMenu:
Level
In the 2 left menus (Omen3 and GemHelper), these are simple menus that are 1 level deep.
In the menu on the right (Postal), this menu has several levels, Postal being the level 1 menu, and the OpenAll submenu being the level 2 menu. The level 3 menu (submenu of OpenAll options) isn't shown.
Display Mode
UIDropDownMenu provides for 2 types of menu display templates.
The GemHelper menu does not have a displayMode set and is the default setting.
The Omen3 and Postal menus have displayMode = "MENU" set.
This is purely for looks, it does not affect functionality.
UIDropDownMenu functions you will use
UIDropDownMenu_AddButton(info, level)
This tells UIDropDownMenu to add a menuitem to the current open menu at level level.
The info argument is a table containing parameters for the menuitem you are adding.
This tells the default UI to open your Dropdown defined by dropDownFrame at level level with submenuID value.
The menu that is displayed will be :SetPoint("TOPLEFT", _G[anchorName], "BOTTOMLEFT", xOffset, yOffset)
If anchorName is the string "cursor", it will be anchored to your cursor instead.
The arguments from anchorName onwards (including anchorName) are all optional.
For the purposes of this guide, menuList and button will not be used or explained.
CloseDropDownMenus([level])
Hides the currently open UIDropDown menu at level level. If level is not specified, it defaults to 1, which means it closes the entire menu.
UIDROPDOWNMENU_OPEN_MENU
This is a global variable in the _G namespace. It will contain the frame reference to the currently open menu (or the last open one).
This variable is rarely used, but can be useful for checking if a certain menu is open to close it, or if you are reusing the same menu frame.
UIDROPDOWNMENU_MENU_VALUE
This is a global variable in the _G namespace. It will contain the submenuID of the deepest level that is currently open/to be opened.
This variable is used to open submenus.
Creating our DropDown
For the purposes of this guide, we will reproduce the Omen menu in the screenshot above.
First we need to create a dropdown frame:
local Omen_TitleDropDownMenu = CreateFrame("Frame", "Omen_TitleDropDownMenu")
Omen_TitleDropDownMenu.displayMode = "MENU"
Omen_TitleDropDownMenu.initialize = function(self, level) end
Yes, it is that simple. Note:
The frame must be named. In this case, it is named "Omen_TitleDropDownMenu" (2nd argument to CreateFrame())
The frame does NOT inherit from any template. It is just a frame. No tricks.
If the .displayMode = "MENU" attribute isn't set, the menu will look like the GemHelper one in the screenshot.
The .initialize attribute is the function that ToggleDropDownMenu() will call when you open the menu.
The .initialize function is THE function that controls the entire menu and its menuitem generation. It takes in 3 arguments, self, level and menuList.
self: This will be the menu frame itself. In our example, the frame Omen_TitleDropDownMenu will be passed in to self. The global UIDROPDOWNMENU_OPEN_MENU will also contain this frame. level: The level of menu that is to be generated/displayed. menuList: We aren't using it in our guide. It will contain menuList passed in to ToggleDropDownMenu()
In order to encourage table reuse, we use a local info table upvalue that we use over and over again to pass information to UIDropDownMenu_AddButton().
local info = {}
Omen_TitleDropDownMenu.initialize = function(self, level)
if not level then return end
wipe(info)
if level == 1 then
-- Create the title of the menu
info.isTitle = 1
info.text = "Omen Quick Menu"
info.notCheckable = 1
UIDropDownMenu_AddButton(info, level)
end
end
When the menu is first called, level will contain the value of 1, since you want to display the level 1 menu. Here, we instruct UIDropDownMenu to add a menuitem with the text "Omen Quick Menu", and that this menuitem is a title.
Here is a little explanation of the menuitem attributes:
info.isTitle = [nil, true] -- If it's a title the button is disabled and the font color is set to yellow
info.text = [STRING] -- The text of the button
info.notCheckable = [nil, 1] -- Shrink the size of the buttons and don't display a check box
Now lets add the remainder menu items.
local info = {}
Omen_TitleDropDownMenu.initialize = function(self, level)
if not level then return end
wipe(info)
if level == 1 then
-- Create the title of the menu
info.isTitle = 1
info.text = "Omen Quick Menu"
info.notCheckable = 1
UIDropDownMenu_AddButton(info, level)
info.disabled = nil
info.isTitle = nil
info.notCheckable = nil
info.text = "Lock Omen"
info.func = function()
db.Locked = not db.Locked
Omen:UpdateGrips()
LibStub("AceConfigRegistry-3.0"):NotifyChange("Omen")
end
info.checked = db.Locked
UIDropDownMenu_AddButton(info, level)
info.text = "Use Focus Target"
info.func = function() Omen:ToggleFocus() end
info.checked = db.UseFocus
UIDropDownMenu_AddButton(info, level)
info.text = "Test Mode"
info.func = function()
testMode = not testMode
Omen:UpdateBars()
LibStub("AceConfigRegistry-3.0"):NotifyChange("Omen")
end
info.checked = testMode
UIDropDownMenu_AddButton(info, level)
info.text = "Open Config"
info.func = function() Omen:ShowConfig() end
info.checked = nil
UIDropDownMenu_AddButton(info, level)
info.text = "Hide Omen"
info.func = function() Omen:Toggle() end
UIDropDownMenu_AddButton(info, level)
-- Close menu item
info.text = CLOSE
info.func = function() CloseDropDownMenus() end
info.checked = nil
info.notCheckable = 1
UIDropDownMenu_AddButton(info, level)
end
end
We do this by setting the .isTitle and .notCheckable options to nil, overwriting the .text attribute, and assign .checked to either true or false depending on whether the menu should display a tick next to it. Note that we also set .disabled to nil, because the function UIDropDownMenu_AddButton() sets .disabled = 1 when .isTitle is true. This is the only case where UIDropDownMenu_AddButton() modifies the info table passed in.
info.disabled = [nil, true] -- Disable the button and show an invisible button that still traps the mouseover event so menu doesn't time out
info.checked = [nil, true, function] -- Check the button if true or function returns true
info.func = [function()] -- The function that is called when you click the button
Looking back at the screenshot above, you will notice that all the menu items are indented except for the title menuitem and the Close menuitem. The indentation is caused by the attribute .notCheckable. If this attribute is nil/false, then it is indented (the button allocates space for the checkmark which is shown or hidden depending on .checked). If it is true/non-nil, then it is not indented. .disabled will cause a button's text to be gray unless .isTitle is also true, which makes it yellow. .colorCode attribute can be used to override this (see Button Attributes at the end of this guide). If .func is nil, then no function is run when the button is clicked.
Opening the menu
Now that we have the menu function done, we want to show the menu. In our case, we want the menu to be shown when we right-click Omen's title bar:
Omen.Title = CreateFrame("Button", "OmenTitle", Omen.Anchor)
-- Other code for the title's width/height/dragging to move Omen etc
Omen.Title:SetScript("OnClick", function(self, button, down)
if button == "RightButton" then
ToggleDropDownMenu(1, nil, Omen_TitleDropDownMenu, self:GetName(), 0, 0)
end
end)
Omen.Title:RegisterForClicks("RightButtonUp")
This tells WoW that "When I right-click OmenTitle, toggle the menu called Omen_TitleDropDownMenu, at level 1, anchored to myself at (0, 0) offset". ToggleDropDownMenu() will then call Omen_TitleDropDownMenu:initialize(1) and your function then adds the menuitems at level 1. This also means that if Omen_TitleDropDownMenu is already open, a second right click on OmenTitle will close the menu.
Common "Mistakes"
You will notice that the menu function above creates new function closures when assigning info.func every time the initialization function is called (when the menu is opened). This isn't really good. You are recommended to upvalue them instead.
Note that the initialization function declaration can also be
function Omen_TitleDropDownMenu:initialize(level)
end
and it will work fine, as long as you realize that the self variable refers to the menu frame, and NOT your addon object. Another mistake is that the function name is also initialize with a small letter i.
So far, the menu items above run functions when clicked on and these functions take in no input arguments. They can however take in arguments. Let's look at how the first part of Postal's menu is created. It is a list of Postal's modules, and the checkmarks indicate whether the modules are currently enabled or disabled.
Yes, the menu items CAN have submenus (see the arrow for Express/Select/OpenAll?) and yet itself be a clickable button that does stuff (in this case, enable/disable the module). This is something DewdropLib can't do.
-- Our Postal menu frame
local Postal_DropDownMenu = CreateFrame("Frame", "Postal_DropDownMenu")
Postal_DropDownMenu.displayMode = "MENU"
Postal_DropDownMenu.info = {}
-- Button that when clicked on shows Postal's menu
-- In Postal, I reuse Postal_DropDownMenu in many places to show different menus
-- So I reassign .initialize to the appropriate menu function before toggling to show it
Postal_ModuleMenuButton:SetScript("OnClick", function(self, button, down)
if Postal_DropDownMenu.initialize ~= Postal.Menu then
CloseDropDownMenus()
Postal_DropDownMenu.initialize = Postal.Menu
end
ToggleDropDownMenu(1, nil, Postal_DropDownMenu, self:GetName(), 0, 0)
end)
function Postal.Menu(self, level)
if not level then return end
local info = self.info
wipe(info)
if level == 1 then
info.isTitle = 1
info.text = "Postal"
info.notCheckable = 1
UIDropDownMenu_AddButton(info, level)
info.disabled = nil
info.isTitle = nil
info.notCheckable = nil
info.keepShownOnClick = 1
for name, module in Postal:IterateModules() do
info.text = L[name]
info.func = Postal.ToggleModule
info.arg1 = name
info.arg2 = module
info.checked = module:IsEnabled()
info.hasArrow = module.ModuleMenu ~= nil
info.value = module
UIDropDownMenu_AddButton(info, level)
end
-- Add a blank separator
wipe(info)
info.disabled = 1
UIDropDownMenu_AddButton(info, level)
info.disabled = nil
end
end
In the code above, we learn how to add a blank separator. Its just a disabled button with no text. For now, ignore the .hasArrow and .value attributes. We also see that we created menu items dynamically, using a for-loop that iterates over all modules.
What we are interested in here is these 2 attributes:
info.arg1 = [ANYTHING] -- This is the first argument used by info.func
info.arg2 = [ANYTHING] -- This is the second argument used by info.func
info.keepShownOnClick = [nil, 1] -- Don't hide the dropdownlist after a button is clicked
Postal.ToggleModule is defined as follows:
function Postal.ToggleModule(dropdownbutton, arg1, arg2, checked)
Postal.db.profile.ModuleEnabledState[arg1] = checked
if checked then arg2:Enable() else arg2:Disable() end
end
The function info.func() is called with the following arguments: dropdownbutton: The button frame that is clicked on. This is something like DropDownList1Button3 which indicates menu level 1 and menu item 3. For the most part, you will never use this, other than to hack checkmark display (see end of guide for UncheckHack). arg1: The value in info.arg1 arg2: The value in info.arg2 checked: Boolean containing true or false. This is the new value that is in effect.
In our example code, we stored the module reference directly in info.arg2, so that we could call arg2:Enable() and arg2:Disable() directly.
The first line saves the new state in the savedvariables.
The second line enables or disables the module.
Creating multiple level menus
This is where we introduce .hasArrow and .value attributes.
info.hasArrow = [nil, true] -- Show the expand arrow for multilevel menus
info.value = [ANYTHING] -- The value that UIDROPDOWNMENU_MENU_VALUE is set to when the button is clicked
When .hasArrow is true, it displays the arrow. When the mouse is hovered over such an entry that has an arrow, the menu initialization function is called to show the submenu with UIDROPDOWNMENU_MENU_VALUE equal to the parent's .value. Here's an example code:
local ExampleDropDownMenu = CreateFrame("Frame", "ExampleDropDownMenu")
ExampleDropDownMenu.displayMode = "MENU"
ExampleDropDownMenu.info = {}
ExampleDropDownMenu.UncheckHack = function(dropdownbutton)
_G[dropdownbutton:GetName().."Check"]:Hide()
end
ExampleDropDownMenu.HideMenu = function()
if UIDROPDOWNMENU_OPEN_MENU == ExampleDropDownMenu then
CloseDropDownMenus()
end
end
ExampleDropDownMenu.initialize = function(self, level)
if not level then return end
local info = self.info
wipe(info)
if level == 1 then
info.isTitle = 1
info.text = "Example"
info.notCheckable = 1
UIDropDownMenu_AddButton(info, level)
info.keepShownOnClick = 1
info.disabled = nil
info.isTitle = nil
info.notCheckable = nil
info.text = "Abcd"
info.func = self.UncheckHack
info.hasArrow = 1
info.value = "submenu1"
UIDropDownMenu_AddButton(info, level)
info.text = "Wxyz" -- Note .hasArrow and .func fallthrough from prev item.
info.value = "submenu2"
UIDropDownMenu_AddButton(info, level)
-- Close menu item
info.hasArrow = nil
info.value = nil
info.notCheckable = 1
info.text = CLOSE
info.func = self.HideMenu
UIDropDownMenu_AddButton(info, level)
elseif level == 2 then
if UIDROPDOWNMENU_MENU_VALUE == "submenu1" then
info.text = "Foo"
UIDropDownMenu_AddButton(info, level)
info.text = "Bar"
UIDropDownMenu_AddButton(info, level)
elseif UIDROPDOWNMENU_MENU_VALUE == "submenu2" then
info.text = "Moo"
UIDropDownMenu_AddButton(info, level)
info.text = "Lar"
UIDropDownMenu_AddButton(info, level)
end
end
end
The above will result in a menu that looks like this screenshot:
.keepShownOnClick = 1 will cause the checkmark to toggle whether you like it or not if you happen to click on it. You can "hack" around it by assigning the UncheckHack as above to the .func which essentally just hides it. If you instead tried to use .notClickable = 1, it will instead disable mouse events on the button, which means that hovering your mouse on it will not open the submenu, only if you hovered your mouse on the arrow itself. So we avoid using it.
Essentially, UIDropDownMenu doesn't care about what your menu parent is, you could have many menu items at level 1 have info.value = "submenu2", and it would open the same "submenu2" at level 2. In particular, info.value can be any value of any type, this means it can be a table, or a number, or a function. You can abuse this by assigning info.value to a table at a higher menu level, so that UIDROPDOWNMENU_MENU_VALUE will be that table at the lower menu level, and iterate over UIDROPDOWNMENU_MENU_VALUE to build the submenu, and assign a subtable to info.value for further submenus.
EasyMenu is meant to be a wrapper provided by Blizzard to help you create menus using UIDropDownMenu. Essentially, instead of writing code to set all the info.blahs and then calling UIDropDownMenu_AddButton(info, level), EasyMenu takes in an array of info[] and does it for you.
The following shows how Omen's menu (in the first screenshot) can be rewritten using EasyMenu. First we have to define a table, lets call it OmenMenuTable. In it, is an array of info[]. So the first menuitem is OmenMenuTable[1], the second menu item is OmenMenuTable[2] and so on.
Simply put, EasyMenu will iterate over the OmenMenuTable and call UIDropDownMenu_AddButton(OmenMenuTable[ i ], 1).
Notice that the .checked entries are now functions that return true/false values. That is because by prepulating the OmenMenuTable, they need to be functions that EasyMenu can call to determine if that item should be checked or not at the time the menu is displayed.
The code to show the menu is simply replacing the ToggleDropDownMenu() to the following EasyMenu() call:
Omen.Title:SetScript("OnClick", function(self, button, down)
if button == "RightButton" then
EasyMenu(OmenMenuTable, Omen_TitleDropDownMenu, self:GetName(), 0, 0, nil)
end
end)
This says, use OmenMenuTable to generate my menu, using the frame Omen_TitleDropDownMenu, and parent it to myself at 0, 0 offsets. The last parameter (nil here) is to indicate the displayMode. nil means to use the GemHelper style displayMode (see first screenshot in first post). "MENU" would be to use the Omen/Postal display style.
There is a catch!
If you use "MENU" display style, your frame MUST inherit from UIDropDownMenuTemplate because EasyMenu() calls UIDropDownMenu_Initialize() which hides certain elements of the inherited frame in "MENU" mode.
That is, the above code has to be changed from
local Omen_TitleDropDownMenu = CreateFrame("Frame", "Omen_TitleDropDownMenu")
Obviously, inheriting from the template will generate a lot of useless inherited frames that get hidden.
Submenus in EasyMenu are defined in
info.menuTable = [TABLE] -- This contains an array of info tables to be displayed as a child menu
So you just embed subtables in your tables as necessary. Obviously .menuTable only has meaning when used with .hasArrow
Advantages of EasyMenu
Easy to code by predefining your info[] tables. No need to write an initialization function.
Good for small static menus.
Disadvantages of EasyMenu
The predefined table is static, meaning you can't dynamically generate menu items to add to your menus, unless you have code that actively modifies the predefined table. This is usually more trouble than it is worth. If you need dynamic menus, code it directly and don't use EasyMenu.
Each .checked entry needs to be a function that returns true/false. This can add up to a lot of functions.
You cannot use displayMode = "MENU" without inheriting from UIDropDownMenuTemplate.
info.value = [ANYTHING] -- The value that UIDROPDOWNMENU_MENU_VALUE is set to when the button is clicked
info.func = [function()] -- The function that is called when you click the button
info.checked = [nil, true, function] -- Check the button if true or function returns true
info.isNotRadio = [nil, true] -- Check the button uses radial image if false check box image if true
info.isTitle = [nil, true] -- If it's a title the button is disabled and the font color is set to yellow
info.disabled = [nil, true] -- Disable the button and show an invisible button that still traps the mouseover event so menu doesn't time out
info.tooltipWhileDisabled = [nil, 1] -- Show the tooltip, even when the button is disabled.
info.hasArrow = [nil, true] -- Show the expand arrow for multilevel menus
info.hasColorSwatch = [nil, true] -- Show color swatch or not, for color selection
info.r = [1 - 255] -- Red color value of the color swatch
info.g = [1 - 255] -- Green color value of the color swatch
info.b = [1 - 255] -- Blue color value of the color swatch
info.colorCode = [STRING] -- "|cAARRGGBB" embedded hex value of the button text color. Only used when button is enabled
info.swatchFunc = [function()] -- Function called by the color picker on color change
info.hasOpacity = [nil, 1] -- Show the opacity slider on the colorpicker frame
info.opacity = [0.0 - 1.0] -- Percentatge of the opacity, 1.0 is fully shown, 0 is transparent
info.opacityFunc = [function()] -- Function called by the opacity slider when you change its value
info.cancelFunc = [function(previousValues)] -- Function called by the colorpicker when you click the cancel button (it takes the previous values as its argument)
info.notClickable = [nil, 1] -- Disable the button and color the font white
info.notCheckable = [nil, 1] -- Shrink the size of the buttons and don't display a check box
info.owner = [Frame] -- Dropdown frame that "owns" the current dropdownlist
info.keepShownOnClick = [nil, 1] -- Don't hide the dropdownlist after a button is clicked
info.tooltipTitle = [nil, STRING] -- Title of the tooltip shown on mouseover
info.tooltipText = [nil, STRING] -- Text of the tooltip shown on mouseover
info.tooltipOnButton = [nil, 1] -- Show the tooltip attached to the button instead of as a Newbie tooltip.
info.justifyH = [nil, "CENTER"] -- Justify button text
info.arg1 = [ANYTHING] -- This is the first argument used by info.func
info.arg2 = [ANYTHING] -- This is the second argument used by info.func
info.fontObject = [FONT] -- font object replacement for Normal and Highlight
info.menuTable = [TABLE] -- This contains an array of info tables to be displayed as a child menu
info.noClickSound = [nil, 1] -- Set to 1 to suppress the sound when clicking the button. The sound only plays if .func is set.
info.padding = [nil, NUMBER] -- Number of pixels to pad the text on the right side
info.minWidth = [nil, NUMBER] -- Minimum width for this line
Notes:
Most of the attributes above are copied to the button frame object representing the menu item. i.e, the dropdownbutton arg that is passed to the first argument of info.func.
.tooltipTitle and .tooltipText are only used when "Beginner Tooltips" cvar is turned on by the user. Sorry, there is no way to provide tooltips otherwise, which is a reason why DewdropLib is popular.
.keepShownOnClick will cause the checkmark to toggle whether you like it or not. You can "hack" around it by assigning the following .func:
ExampleDropDownMenu.UncheckHack = function(dropdownbutton)
_G[dropdownbutton:GetName().."Check"]:Hide()
end
-- In the code for the menu items...
info.keepShownOnClick = 1
info.func = self.UncheckHack
.owner isn't used by the default UI, or by any code for the matter.
Read FrameXML\UIDropDownMenu.lua code to see how the remainer attributes are used. The only ones I haven't covered is really just the color swatch (example where it is used is in the right click menu on your chatframe tab to set the background color).
Conclusion
Using UIDropDownMenu is relatively simple. All it requires is to create a named frame, and a menu initialization function. It is well suited for small menus that have very few items and/or very few levels. If you need something more, it is probably better to use DewdropLib or some form of UIDropDownMenu wrapper that can do most of these tasks automatically rather than tediously set info.blah everytime.
good job Xinhuan will help for sure but yet im not enough convinced to do the jump from dew which does not need lots of tweaks to run along ace3config, I'm rather waiting at xinhuan's dewdrop3 ;p
Should mention the taint issues with UIDropDownMenu.
That is a different issue and has been longstanding. As long as you don't use "Set Focus" from the menu in unit frames, taint is irrelevant. So many addons use UIDropDownMenu that this isn't really a valid reason for your addon not to use one.
good job Xinhuan will help for sure but yet im not enough convinced to do the jump from dew which does not need lots of tweaks to run along ace3config, I'm rather waiting at xinhuan's dewdrop3 ;p
UIDropDownMenu is meant for simple menus, usually menus that are just 1 or 2 levels deep, for small simple options or executing simple functions.
If you look at Omen's menu, it doesn't configure stuff, it is just a quick menu that offers quick toggles to commonly used functions. Omen still uses AceGUI/AceConfig for the heavyweight configuration.
Recount is another addon that is using UIDropDownMenu in exactly the same way as described above as Omen.
Talented use EasyMenu() to display partially dynamic menus. The code is based around an internal repository of menu tables, indexed by names. A function creates once the static structure of the complete menu, and another updates the part that needs updating. For instance :
function Talented:CreateActionMenu()
...
menu[#menu + 1] = {
text = L["Export template"],
hasArrow = true,
menuList = self:GetNamedMenu("exporters"),
}
...
end
function Talented:MakeActionMenu()
...
local exporters = self:GetNamedMenu("exporters")
local index = 1
for name, handler in pairs(self.exporters) do
exporters[index] = exporters[index] or {}
exporters[index].text = name
exporters[index].func = Export_Template
exporters[index].arg1 = handler
index = index + 1
end
for i = index, #exporters do
exporters[i].text = nil
end
...
end
Omen.Title = CreateFrame("Button", "OmenTitle", Omen.Anchor)
-- Other code for the title's width/height/dragging to move Omen etc
Omen.Title:SetScript("OnClick", function(self, button, down)
if button == "RightButton" then
ToggleDropDownMenu(1, nil, Omen_TitleDropDownMenu, self:GetName(), 0, 0)
end
end)
Omen.Title:RegisterForClicks("RightButtonUp")
have been messing with UIDropDownMenu and had a quick question is it necessary to use a named frame to use ToggleDropDownMenu in your example you use self:GetName() from what i gather from blizzard's code is that simply passing self should be sufficient (and works so far without error on my tests)
Omen.Title = CreateFrame("Button", "OmenTitle", Omen.Anchor)
-- Other code for the title's width/height/dragging to move Omen etc
Omen.Title:SetScript("OnClick", function(self, button, down)
if button == "RightButton" then
ToggleDropDownMenu(1, nil, Omen_TitleDropDownMenu, [B]self:GetName()[/B], 0, 0)
end
end)
Omen.Title:RegisterForClicks("RightButtonUp")
have been messing with UIDropDownMenu and had a quick question is it necessary to use a named frame to use ToggleDropDownMenu in your example you use self:GetName() from what i gather from blizzard's code is that simply passing self should be sufficient (and works so far without error on my tests)
It is not necessary. You can use both self or self:GetName().
--This seems to dynamically change the text of the button in an open menu. colorCode in this context is the |cffrrbbgg and the color code will need to include the |c opening. Although it probably will be easier to just include that as part of the text value.
Also as for checked options, note that the function provided to call (info.func) when the button is clicked is passed the "checked" value as arg4. Naturally there is no UIDropDwonMenuButton_IsChecked function for it.
Using EasyMenu
[...]
Notice that the .checked entries are now functions that return true/false values. That is because by prepulating the OmenMenuTable,
"Prepulating" sounds like something which may be illegal in the city where I live.
In the code above, we learn how to add a blank separator. Its just a disabled button with no text.
Btw, this doesn't work anymore. The button gets collapsed to zero height and doesn't separate anything. Explicitly setting text="" along with disabling it works.
Also, the comments in UIDropDownMenu.lua are wrong; the field for forming subtables is called .menuList, not .menuTable
I'm in the process of removing Dewdrop from Revelation. Doing so required some evil hackery, as I relied on its always-present tooltips and the ability to set hyperlinks on a tooltip.
Here is what I have at present:
local AddDropDownButton
do
local hooks = {}
hooksecurefunc(DropDownList1, "Hide",
function()
for button, data in pairs(hooks) do
if data.OnEnter then
button:SetScript("OnEnter", data.OnEnter)
data.OnEnter = nil
end
if data.OnLeave then
button:SetScript("OnLeave", data.OnLeave)
data.OnLeave = nil
end
end
end)
local function DropDownButton_OnEnter(self, ...)
local data = hooks[self]
if not data then
return
end
if data.OnEnter then
data.OnEnter(self, ...)
end
if data.tooltip_func then
GameTooltip_SetDefaultAnchor(GameTooltip, self)
data.tooltip_func(data.tooltip_arg1, data.tooltip_arg2)
GameTooltip:Show()
elseif data.tooltip_text then
GameTooltip_SetDefaultAnchor(GameTooltip, self)
GameTooltip:AddLine(data.tooltip_text or "Foof")
GameTooltip:Show()
end
end
local function DropDownButton_OnLeave(self, ...)
local data = hooks[self]
if data and data.OnLeave then
data.OnLeave(self, ...)
end
GameTooltip:Hide()
end
function AddDropDownButton(info, level)
UIDropDownMenu_AddButton(info, level)
local listFrame = _G["DropDownList"..level]
local button_name = listFrame:GetName().."Button"..(listFrame.numButtons or 1)
local button = _G[button_name]
local entry = hooks[button] or {}
-- If there are no backup scripts, make them and then set new ones.
if not entry.OnEnter then
entry.OnEnter = button:GetScript("OnEnter")
button:SetScript("OnEnter", DropDownButton_OnEnter)
end
if not entry.OnLeave then
entry.OnLeave = button:GetScript("OnLeave")
button:SetScript("OnLeave", DropDownButton_OnLeave)
end
entry.name = button_name
entry.tooltip_text = info.tooltip_text
entry.tooltip_func = info.tooltip_func
entry.tooltip_arg1 = info.tooltip_arg1
entry.tooltip_arg2 = info.tooltip_arg2
entry.level = level
hooks[button] = entry
end
end -- do
EDIT: Using this code, I have tooltips even when beginner tooltips are toggled off, and I can set hyperlinks on tooltips for recipes, spells, and equipment. If there is interest in this, it could be made into a library.
The purpose of this guide is to aid authors in creating simple dropdown menus that are using the default UI's built in UIDropDownMenu without requiring any library.
Limitations of UIDropDownMenu
Advantages of UIDropDownMenu
Some Terminology
Look at the following dropdown menus that are created by using UIDropDownMenu:
Level
In the 2 left menus (Omen3 and GemHelper), these are simple menus that are 1 level deep.
In the menu on the right (Postal), this menu has several levels, Postal being the level 1 menu, and the OpenAll submenu being the level 2 menu. The level 3 menu (submenu of OpenAll options) isn't shown.
Display Mode
UIDropDownMenu provides for 2 types of menu display templates.
The GemHelper menu does not have a displayMode set and is the default setting.
The Omen3 and Postal menus have displayMode = "MENU" set.
This is purely for looks, it does not affect functionality.
UIDropDownMenu functions you will use
UIDropDownMenu_AddButton(info, level)
This tells UIDropDownMenu to add a menuitem to the current open menu at level level.
The info argument is a table containing parameters for the menuitem you are adding.
ToggleDropDownMenu(level, value, dropDownFrame, anchorName, xOffset, yOffset, menuList, button)
This tells the default UI to open your Dropdown defined by dropDownFrame at level level with submenuID value.
The menu that is displayed will be :SetPoint("TOPLEFT", _G[anchorName], "BOTTOMLEFT", xOffset, yOffset)
If anchorName is the string "cursor", it will be anchored to your cursor instead.
The arguments from anchorName onwards (including anchorName) are all optional.
For the purposes of this guide, menuList and button will not be used or explained.
CloseDropDownMenus([level])
Hides the currently open UIDropDown menu at level level. If level is not specified, it defaults to 1, which means it closes the entire menu.
UIDROPDOWNMENU_OPEN_MENU
This is a global variable in the _G namespace. It will contain the frame reference to the currently open menu (or the last open one).
This variable is rarely used, but can be useful for checking if a certain menu is open to close it, or if you are reusing the same menu frame.
UIDROPDOWNMENU_MENU_VALUE
This is a global variable in the _G namespace. It will contain the submenuID of the deepest level that is currently open/to be opened.
This variable is used to open submenus.
Creating our DropDown
For the purposes of this guide, we will reproduce the Omen menu in the screenshot above.
First we need to create a dropdown frame:
Yes, it is that simple. Note:
The .initialize function is THE function that controls the entire menu and its menuitem generation. It takes in 3 arguments, self, level and menuList.
self: This will be the menu frame itself. In our example, the frame Omen_TitleDropDownMenu will be passed in to self. The global UIDROPDOWNMENU_OPEN_MENU will also contain this frame.
level: The level of menu that is to be generated/displayed.
menuList: We aren't using it in our guide. It will contain menuList passed in to ToggleDropDownMenu()
In order to encourage table reuse, we use a local info table upvalue that we use over and over again to pass information to UIDropDownMenu_AddButton().
When the menu is first called, level will contain the value of 1, since you want to display the level 1 menu. Here, we instruct UIDropDownMenu to add a menuitem with the text "Omen Quick Menu", and that this menuitem is a title.
Here is a little explanation of the menuitem attributes:
info.isTitle = [nil, true] -- If it's a title the button is disabled and the font color is set to yellow
info.text = [STRING] -- The text of the button
info.notCheckable = [nil, 1] -- Shrink the size of the buttons and don't display a check box
Now lets add the remainder menu items.
We do this by setting the .isTitle and .notCheckable options to nil, overwriting the .text attribute, and assign .checked to either true or false depending on whether the menu should display a tick next to it. Note that we also set .disabled to nil, because the function UIDropDownMenu_AddButton() sets .disabled = 1 when .isTitle is true. This is the only case where UIDropDownMenu_AddButton() modifies the info table passed in.
info.disabled = [nil, true] -- Disable the button and show an invisible button that still traps the mouseover event so menu doesn't time out
info.checked = [nil, true, function] -- Check the button if true or function returns true
info.func = [function()] -- The function that is called when you click the button
Looking back at the screenshot above, you will notice that all the menu items are indented except for the title menuitem and the Close menuitem. The indentation is caused by the attribute .notCheckable. If this attribute is nil/false, then it is indented (the button allocates space for the checkmark which is shown or hidden depending on .checked). If it is true/non-nil, then it is not indented. .disabled will cause a button's text to be gray unless .isTitle is also true, which makes it yellow. .colorCode attribute can be used to override this (see Button Attributes at the end of this guide). If .func is nil, then no function is run when the button is clicked.
Opening the menu
Now that we have the menu function done, we want to show the menu. In our case, we want the menu to be shown when we right-click Omen's title bar:
This tells WoW that "When I right-click OmenTitle, toggle the menu called Omen_TitleDropDownMenu, at level 1, anchored to myself at (0, 0) offset". ToggleDropDownMenu() will then call Omen_TitleDropDownMenu:initialize(1) and your function then adds the menuitems at level 1. This also means that if Omen_TitleDropDownMenu is already open, a second right click on OmenTitle will close the menu.
Common "Mistakes"
You will notice that the menu function above creates new function closures when assigning info.func every time the initialization function is called (when the menu is opened). This isn't really good. You are recommended to upvalue them instead.
Note that the initialization function declaration can also be
and it will work fine, as long as you realize that the self variable refers to the menu frame, and NOT your addon object. Another mistake is that the function name is also initialize with a small letter i.
So far, the menu items above run functions when clicked on and these functions take in no input arguments. They can however take in arguments. Let's look at how the first part of Postal's menu is created. It is a list of Postal's modules, and the checkmarks indicate whether the modules are currently enabled or disabled.
Yes, the menu items CAN have submenus (see the arrow for Express/Select/OpenAll?) and yet itself be a clickable button that does stuff (in this case, enable/disable the module). This is something DewdropLib can't do.
In the code above, we learn how to add a blank separator. Its just a disabled button with no text. For now, ignore the .hasArrow and .value attributes. We also see that we created menu items dynamically, using a for-loop that iterates over all modules.
What we are interested in here is these 2 attributes:
info.arg1 = [ANYTHING] -- This is the first argument used by info.func
info.arg2 = [ANYTHING] -- This is the second argument used by info.func
info.keepShownOnClick = [nil, 1] -- Don't hide the dropdownlist after a button is clicked
Postal.ToggleModule is defined as follows:
The function info.func() is called with the following arguments:
dropdownbutton: The button frame that is clicked on. This is something like DropDownList1Button3 which indicates menu level 1 and menu item 3. For the most part, you will never use this, other than to hack checkmark display (see end of guide for UncheckHack).
arg1: The value in info.arg1
arg2: The value in info.arg2
checked: Boolean containing true or false. This is the new value that is in effect.
In our example code, we stored the module reference directly in info.arg2, so that we could call arg2:Enable() and arg2:Disable() directly.
The first line saves the new state in the savedvariables.
The second line enables or disables the module.
Creating multiple level menus
This is where we introduce .hasArrow and .value attributes.
info.hasArrow = [nil, true] -- Show the expand arrow for multilevel menus
info.value = [ANYTHING] -- The value that UIDROPDOWNMENU_MENU_VALUE is set to when the button is clicked
When .hasArrow is true, it displays the arrow. When the mouse is hovered over such an entry that has an arrow, the menu initialization function is called to show the submenu with UIDROPDOWNMENU_MENU_VALUE equal to the parent's .value. Here's an example code:
The above will result in a menu that looks like this screenshot:
.keepShownOnClick = 1 will cause the checkmark to toggle whether you like it or not if you happen to click on it. You can "hack" around it by assigning the UncheckHack as above to the .func which essentally just hides it. If you instead tried to use .notClickable = 1, it will instead disable mouse events on the button, which means that hovering your mouse on it will not open the submenu, only if you hovered your mouse on the arrow itself. So we avoid using it.
Essentially, UIDropDownMenu doesn't care about what your menu parent is, you could have many menu items at level 1 have info.value = "submenu2", and it would open the same "submenu2" at level 2. In particular, info.value can be any value of any type, this means it can be a table, or a number, or a function. You can abuse this by assigning info.value to a table at a higher menu level, so that UIDROPDOWNMENU_MENU_VALUE will be that table at the lower menu level, and iterate over UIDROPDOWNMENU_MENU_VALUE to build the submenu, and assign a subtable to info.value for further submenus.
EasyMenu is meant to be a wrapper provided by Blizzard to help you create menus using UIDropDownMenu. Essentially, instead of writing code to set all the info.blahs and then calling UIDropDownMenu_AddButton(info, level), EasyMenu takes in an array of info[] and does it for you.
The following shows how Omen's menu (in the first screenshot) can be rewritten using EasyMenu. First we have to define a table, lets call it OmenMenuTable. In it, is an array of info[]. So the first menuitem is OmenMenuTable[1], the second menu item is OmenMenuTable[2] and so on.
Simply put, EasyMenu will iterate over the OmenMenuTable and call UIDropDownMenu_AddButton(OmenMenuTable[ i ], 1).
Notice that the .checked entries are now functions that return true/false values. That is because by prepulating the OmenMenuTable, they need to be functions that EasyMenu can call to determine if that item should be checked or not at the time the menu is displayed.
The code to show the menu is simply replacing the ToggleDropDownMenu() to the following EasyMenu() call:
This says, use OmenMenuTable to generate my menu, using the frame Omen_TitleDropDownMenu, and parent it to myself at 0, 0 offsets. The last parameter (nil here) is to indicate the displayMode. nil means to use the GemHelper style displayMode (see first screenshot in first post). "MENU" would be to use the Omen/Postal display style.
There is a catch!
If you use "MENU" display style, your frame MUST inherit from UIDropDownMenuTemplate because EasyMenu() calls UIDropDownMenu_Initialize() which hides certain elements of the inherited frame in "MENU" mode.
That is, the above code has to be changed from
and
to
and
Obviously, inheriting from the template will generate a lot of useless inherited frames that get hidden.
Submenus in EasyMenu are defined in
info.menuTable = [TABLE] -- This contains an array of info tables to be displayed as a child menu
So you just embed subtables in your tables as necessary. Obviously .menuTable only has meaning when used with .hasArrow
Advantages of EasyMenu
Button attributes
The following is cut and pasted from FrameXML\UIDropDownMenu.lua
Notes:
Most of the attributes above are copied to the button frame object representing the menu item. i.e, the dropdownbutton arg that is passed to the first argument of info.func.
.tooltipTitle and .tooltipText are only used when "Beginner Tooltips" cvar is turned on by the user. Sorry, there is no way to provide tooltips otherwise, which is a reason why DewdropLib is popular.
.keepShownOnClick will cause the checkmark to toggle whether you like it or not. You can "hack" around it by assigning the following .func:
.owner isn't used by the default UI, or by any code for the matter.
Read FrameXML\UIDropDownMenu.lua code to see how the remainer attributes are used. The only ones I haven't covered is really just the color swatch (example where it is used is in the right click menu on your chatframe tab to set the background color).
Conclusion
Using UIDropDownMenu is relatively simple. All it requires is to create a named frame, and a menu initialization function. It is well suited for small menus that have very few items and/or very few levels. If you need something more, it is probably better to use DewdropLib or some form of UIDropDownMenu wrapper that can do most of these tasks automatically rather than tediously set info.blah everytime.
Also, never ever use it for SharedMedia textures or fonts >:(
Great job.
That is a different issue and has been longstanding. As long as you don't use "Set Focus" from the menu in unit frames, taint is irrelevant. So many addons use UIDropDownMenu that this isn't really a valid reason for your addon not to use one.
UIDropDownMenu is meant for simple menus, usually menus that are just 1 or 2 levels deep, for small simple options or executing simple functions.
If you look at Omen's menu, it doesn't configure stuff, it is just a quick menu that offers quick toggles to commonly used functions. Omen still uses AceGUI/AceConfig for the heavyweight configuration.
Recount is another addon that is using UIDropDownMenu in exactly the same way as described above as Omen.
have been messing with UIDropDownMenu and had a quick question is it necessary to use a named frame to use ToggleDropDownMenu in your example you use self:GetName() from what i gather from blizzard's code is that simply passing self should be sufficient (and works so far without error on my tests)
It is not necessary. You can use both self or self:GetName().
Menu Functions
--Changes the visible width of the drop down template. This function moves all the textures into the right place for the desired width.
--Sets the visible text on the drop down when it's closed.
--Obvious. Gets the visible text on the drop down when it's closed.
--Adjusts the location / justification of the visible text while closed.
Unfortunately there is no UIDropDownMenu_SetHeight function.
Button Functions
Used in the info.func function.
--Used to get the name of the selected option in the drop down while open. Tho you might already have this.
--This seems to dynamically change the text of the button in an open menu. colorCode in this context is the |cffrrbbgg and the color code will need to include the |c opening. Although it probably will be easier to just include that as part of the text value.
Also as for checked options, note that the function provided to call (info.func) when the button is clicked is passed the "checked" value as arg4. Naturally there is no UIDropDwonMenuButton_IsChecked function for it.
"Prepulating" sounds like something which may be illegal in the city where I live.
:-p
Thank you for this guide!
Btw, this doesn't work anymore. The button gets collapsed to zero height and doesn't separate anything. Explicitly setting text="" along with disabling it works.
Also, the comments in UIDropDownMenu.lua are wrong; the field for forming subtables is called .menuList, not .menuTable
Here is what I have at present:
EDIT: Using this code, I have tooltips even when beginner tooltips are toggled off, and I can set hyperlinks on tooltips for recipes, spells, and equipment. If there is interest in this, it could be made into a library.