|
|
#1 |
|
Asian Sheep Lover
Join Date: Aug 2007
Location: Singapore
Posts: 3,556
|
Introduction
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: Code:
local Omen_TitleDropDownMenu = CreateFrame("Frame", "Omen_TitleDropDownMenu")
Omen_TitleDropDownMenu.displayMode = "MENU"
Omen_TitleDropDownMenu.initialize = function(self, level) end
__________________
Author/Maintainer of Postal, Omen3, GemHelper, BankItems, WoWEquip, GatherMate, Routes, HandyNotes and some others. |
|
|
|
|
|
#2 |
|
Asian Sheep Lover
Join Date: Aug 2007
Location: Singapore
Posts: 3,556
|
Writing our initialize menu function
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(). Code:
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
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. Code:
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
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: Code:
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")
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 Code:
function Omen_TitleDropDownMenu:initialize(level) end
__________________
Author/Maintainer of Postal, Omen3, GemHelper, BankItems, WoWEquip, GatherMate, Routes, HandyNotes and some others. Last edited by Xinhuan; 02-03-2009 at 02:01 PM. |
|
|
|
|
|
#3 |
|
Asian Sheep Lover
Join Date: Aug 2007
Location: Singapore
Posts: 3,556
|
Passing arguments to functions
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. Code:
-- 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
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: Code:
function Postal.ToggleModule(dropdownbutton, arg1, arg2, checked)
Postal.db.profile.ModuleEnabledState[arg1] = checked
if checked then arg2:Enable() else arg2:Disable() end
end
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: 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
.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.
__________________
Author/Maintainer of Postal, Omen3, GemHelper, BankItems, WoWEquip, GatherMate, Routes, HandyNotes and some others. |
|
|
|
|
|
#4 |
|
Asian Sheep Lover
Join Date: Aug 2007
Location: Singapore
Posts: 3,556
|
Using EasyMenu
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). Code:
local Omen_TitleDropDownMenu = CreateFrame("Frame", "Omen_TitleDropDownMenu")
local OmenMenuTable = {
{
text = L["Omen Quick Menu"],
isTitle = 1,
notCheckable = 1,
},
{
text = L["Lock Omen"],
func = function()
db.Locked = not db.Locked
Omen:UpdateGrips()
LibStub("AceConfigRegistry-3.0"):NotifyChange("Omen")
end,
checked = function() return db.Locked end,
},
{
text = L["Use Focus Target"],
func = function() Omen:ToggleFocus() end,
checked = function() return db.UseFocus end,
},
{
text = L["Test Mode"],
func = function()
testMode = not testMode
Omen:UpdateBars()
LibStub("AceConfigRegistry-3.0"):NotifyChange("Omen")
end,
checked = function() return testMode end,
},
{
text = L["Open Config"],
func = function() Omen:ShowConfig() end,
},
{
text = L["Hide Omen"],
func = function() Omen:Toggle() end,
},
{
text = CLOSE,
func = function() CloseDropDownMenus() end,
notCheckable = 1,
},
}
The code to show the menu is simply replacing the ToggleDropDownMenu() to the following EasyMenu() call: Code:
Omen.Title:SetScript("OnClick", function(self, button, down)
if button == "RightButton" then
EasyMenu(OmenMenuTable, Omen_TitleDropDownMenu, self:GetName(), 0, 0, nil)
end
end)
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 Code:
local Omen_TitleDropDownMenu = CreateFrame("Frame", "Omen_TitleDropDownMenu")
and
EasyMenu(OmenMenuTable, Omen_TitleDropDownMenu, self:GetName(), 0, 0, nil)
Code:
local Omen_TitleDropDownMenu = CreateFrame("Frame", "Omen_TitleDropDownMenu", nil, "UIDropDownMenuTemplate")
and
EasyMenu(OmenMenuTable, Omen_TitleDropDownMenu, self:GetName(), 0, 0, "MENU")
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
Disadvantages of EasyMenu
Button attributes The following is cut and pasted from FrameXML\UIDropDownMenu.lua --[[ List of button attributes ================================================== ==== info.text = [STRING] -- The text of the button 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.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.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.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 ]] 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: Code:
ExampleDropDownMenu.UncheckHack = function(dropdownbutton)
_G[dropdownbutton:GetName().."Check"]:Hide()
end
-- In the code for the menu items...
info.keepShownOnClick = 1
info.func = self.UncheckHack
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.
__________________
Author/Maintainer of Postal, Omen3, GemHelper, BankItems, WoWEquip, GatherMate, Routes, HandyNotes and some others. Last edited by Xinhuan; 02-03-2009 at 03:29 PM. |
|
|
|
|
|
#5 |
|
Legendary Member
Join Date: Dec 2005
Location: Seattle
Posts: 3,367
|
Should mention the taint issues with UIDropDownMenu.
Also, never ever use it for SharedMedia textures or fonts ![]() |
|
|
|
|
|
#6 |
|
Full Member
|
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
|
|
|
|
|
|
#7 |
|
Senior Member
Join Date: Nov 2005
Posts: 328
|
This should be on wowwiki and pretty much any addon hosting site that has a developer forum
![]() Great job. |
|
|
|
|
|
#8 |
|
Asian Sheep Lover
Join Date: Aug 2007
Location: Singapore
Posts: 3,556
|
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.
__________________
Author/Maintainer of Postal, Omen3, GemHelper, BankItems, WoWEquip, GatherMate, Routes, HandyNotes and some others. |
|
|
|
|
|
#9 | |
|
Asian Sheep Lover
Join Date: Aug 2007
Location: Singapore
Posts: 3,556
|
Quote:
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.
__________________
Author/Maintainer of Postal, Omen3, GemHelper, BankItems, WoWEquip, GatherMate, Routes, HandyNotes and some others. |
|
|
|
|
|
|
#10 |
|
Asian Sheep Lover
Join Date: Aug 2007
Location: Singapore
Posts: 3,556
|
I have updated the last post in the guide with how to use EasyMenu.
__________________
Author/Maintainer of Postal, Omen3, GemHelper, BankItems, WoWEquip, GatherMate, Routes, HandyNotes and some others. |
|
|
|
![]() |
«
Previous Thread
|
Next Thread
»
| Currently Active Users Viewing This Thread: 1 (0 members and 1 guests) | |
| Thread Tools | |
|
|
All times are GMT. The time now is 05:03 PM.
WowAce Forums








